Fix remaining Dutch string in DataQualityView
This commit is contained in:
+15
-15
@@ -105,9 +105,9 @@ AGENTS = [
|
|||||||
"motto": "Guardian of every row",
|
"motto": "Guardian of every row",
|
||||||
"capabilities": ["PostgreSQL", "MySQL", "MongoDB", "Cassandra", "Neo4j"],
|
"capabilities": ["PostgreSQL", "MySQL", "MongoDB", "Cassandra", "Neo4j"],
|
||||||
"suggested_prompts": [
|
"suggested_prompts": [
|
||||||
"Hoeveel data zit er in de databases?",
|
"How much data is in the databases?",
|
||||||
"Wat staat er in PostgreSQL sales_orders?",
|
"What is in PostgreSQL sales_orders?",
|
||||||
"MongoDB supplychain overzicht",
|
"MongoDB supplychain overview",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -332,21 +332,21 @@ async def ask_llm(
|
|||||||
log: Any | None = None,
|
log: Any | None = None,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
agent = next(a for a in AGENTS if a["id"] == agent_id)
|
agent = next(a for a in AGENTS if a["id"] == agent_id)
|
||||||
system = f"""Je bent {agent['name']}, een autonomous ops agent in het Dell ATC data lab.
|
system = f"""You are {agent['name']}, an autonomous ops agent in the Dell ATC data lab.
|
||||||
Specialisatie: {agent['role']}.
|
Specialization: {agent['role']}.
|
||||||
Motto: {agent.get('motto', '')}
|
Motto: {agent.get('motto', '')}.
|
||||||
|
|
||||||
Je antwoordt namens je domein maar hebt zicht op de HELE lab stack: Docker, databases, lakehouse (Trino/Spark/Kafka Connect), ETL (Airflow/Kafka), Hadoop HDFS, en GPU/vLLM.
|
You respond on behalf of your domain but have visibility into the FULL lab stack: Docker, databases, lakehouse (Trino/Spark/Kafka Connect), ETL (Airflow/Kafka), Hadoop HDFS, and GPU/vLLM.
|
||||||
|
|
||||||
Regels:
|
Rules:
|
||||||
- Antwoord in dezelfde taal als de gebruiker (Nederlands of Engels).
|
- Always respond in English.
|
||||||
- Je hebt volledige zicht op de HELE cluster: alle VMs, zones, connectors, GPU, Hadoop, ObjectScale en Command Center.
|
- You have full visibility into the entire cluster: all VMs, zones, connectors, GPU, Hadoop, ObjectScale and Command Center.
|
||||||
- Gebruik ALLEEN de live data hieronder — verzin geen hosts, poorten, cijfers of connector namen.
|
- Use ONLY the live data below — do not invent hosts, ports, numbers or connector names.
|
||||||
- Gebruik exact de container/connector namen uit de data (bijv. mysql-hr-connector, niet "Debezium").
|
- Use exact container/connector names from the data (e.g. mysql-hr-connector, not "Debezium").
|
||||||
- Als iets DOWN of 0 GB is, zeg dat eerlijk.
|
- If something is DOWN or 0 GB, say so honestly.
|
||||||
- Kort en behulpzaam (max ~10 zinnen); bullet lists mogen als het overzicht helpt.
|
- Be concise and helpful (max ~10 sentences); bullet lists are fine when they aid clarity.
|
||||||
|
|
||||||
--- LIVE LAB DATA (primary domain eerst, daarna volledige stack) ---
|
--- LIVE LAB DATA (primary domain first, then full stack) ---
|
||||||
{context}
|
{context}
|
||||||
"""
|
"""
|
||||||
if log:
|
if log:
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function ActivityStream({ feed, agents, filterAgentId, opsOnly }: Props)
|
|||||||
<div className="scrollbar-thin flex-1 overflow-y-auto px-2 pb-2">
|
<div className="scrollbar-thin flex-1 overflow-y-auto px-2 pb-2">
|
||||||
{items.length === 0 && (
|
{items.length === 0 && (
|
||||||
<p className="py-8 text-center text-[11px] text-foreground-faint">
|
<p className="py-8 text-center text-[11px] text-foreground-faint">
|
||||||
{opsOnly ? 'Geen operationele events — antwords staan in Chat.' : 'No activity yet — agents are on standby.'}
|
{opsOnly ? 'No operational events — responses appear in Chat.' : 'No activity yet — agents are on standby.'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{items.map((e) => {
|
{items.map((e) => {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function ChatDrawer({
|
|||||||
Chat & Activity
|
Chat & Activity
|
||||||
{unread > 0 && (
|
{unread > 0 && (
|
||||||
<span className="rounded-full bg-docker/20 px-2 py-0.5 font-mono text-[10px] text-docker">
|
<span className="rounded-full bg-docker/20 px-2 py-0.5 font-mono text-[10px] text-docker">
|
||||||
{unread} bericht{unread !== 1 ? 'en' : ''}
|
{unread} message{unread !== 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{approvals.length > 0 && (
|
{approvals.length > 0 && (
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ export function CommandBar({ busy, selectedAgent, onSubmit }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const suggestions = selectedAgent?.suggested_prompts?.slice(0, 3) || [
|
const suggestions = selectedAgent?.suggested_prompts?.slice(0, 3) || [
|
||||||
'Hoeveel data zit er in de databases?',
|
'How much data is in the databases?',
|
||||||
'Wat staat er in PostgreSQL?',
|
'What is in PostgreSQL?',
|
||||||
'MongoDB supplychain overzicht',
|
'MongoDB supplychain overview',
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function CommsPanel({ messages, agents, selectedAgent, busy }: Props) {
|
|||||||
{busy && (
|
{busy && (
|
||||||
<div className="flex items-center gap-2 px-2 py-2 text-[10px] text-foreground-muted">
|
<div className="flex items-center gap-2 px-2 py-2 text-[10px] text-foreground-muted">
|
||||||
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-accent" />
|
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-accent" />
|
||||||
<span>Agent verzamelt cluster-data en vraagt Llama 70B… verwacht ~30–90 sec</span>
|
<span>Agent gathering cluster data and querying Llama 70B… expect ~30–90 sec</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={bottomRef} />
|
<div ref={bottomRef} />
|
||||||
|
|||||||
@@ -37,8 +37,36 @@ type ColumnProfile = {
|
|||||||
top_values?: { value: string; count: number }[]
|
top_values?: { value: string; count: number }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type GxCheck = { suite: string; expectation: string; success: boolean; result: string; column?: string }
|
type Violations = {
|
||||||
type SodaCheck = { suite: string; name: string; check: string; outcome: string; detail: string }
|
violation_count?: number
|
||||||
|
violation_pct?: number
|
||||||
|
sample_rows?: { row_index: number; values: Record<string, unknown> }[]
|
||||||
|
location_hint?: string
|
||||||
|
affected_columns?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type DqCheck = {
|
||||||
|
id?: string
|
||||||
|
suite: string
|
||||||
|
success?: boolean
|
||||||
|
status?: string
|
||||||
|
outcome?: string
|
||||||
|
expectation?: string
|
||||||
|
name?: string
|
||||||
|
metric?: string
|
||||||
|
check?: string
|
||||||
|
test?: string
|
||||||
|
rule?: string
|
||||||
|
monitor?: string
|
||||||
|
result?: string
|
||||||
|
detail?: string
|
||||||
|
column?: string
|
||||||
|
violations?: Violations
|
||||||
|
meta?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
type GxCheck = DqCheck & { expectation: string; success: boolean; result: string }
|
||||||
|
type SodaCheck = DqCheck & { name: string; check: string; outcome: string; detail: string }
|
||||||
|
|
||||||
type DocStructure = {
|
type DocStructure = {
|
||||||
pages: number
|
pages: number
|
||||||
@@ -67,10 +95,24 @@ type AssessResult = {
|
|||||||
dimensions: Dimension[]
|
dimensions: Dimension[]
|
||||||
column_profiles: ColumnProfile[]
|
column_profiles: ColumnProfile[]
|
||||||
action_items: { priority: string; dimension: string; score: number; action: string }[]
|
action_items: { priority: string; dimension: string; score: number; action: string }[]
|
||||||
checks?: { great_expectations: GxCheck[]; soda_core: SodaCheck[] }
|
checks?: {
|
||||||
checks_summary: {
|
great_expectations: GxCheck[]
|
||||||
great_expectations: { total: number; passed: number }
|
soda_core: SodaCheck[]
|
||||||
soda_core: { total: number; warnings: number }
|
deequ?: DqCheck[]
|
||||||
|
pandera?: DqCheck[]
|
||||||
|
dbt_expectations?: DqCheck[]
|
||||||
|
monte_carlo?: DqCheck[]
|
||||||
|
affirm?: DqCheck[]
|
||||||
|
}
|
||||||
|
checks_summary: Record<string, { total: number; passed?: number; warnings?: number }>
|
||||||
|
executive_summary?: {
|
||||||
|
headline: string
|
||||||
|
dataset: string
|
||||||
|
checks_run: number
|
||||||
|
issues_found: number
|
||||||
|
flagged_columns: number
|
||||||
|
tools: string[]
|
||||||
|
recommendation: string
|
||||||
}
|
}
|
||||||
docling?: { used: boolean; parse_id?: string; document_structure?: DocStructure; stats?: Record<string, number>; images?: DocImage[] }
|
docling?: { used: boolean; parse_id?: string; document_structure?: DocStructure; stats?: Record<string, number>; images?: DocImage[] }
|
||||||
rag_ingest?: { ok: boolean; duplicate?: boolean; chunks?: number; message?: string; error?: string }
|
rag_ingest?: { ok: boolean; duplicate?: boolean; chunks?: number; message?: string; error?: string }
|
||||||
@@ -140,8 +182,9 @@ export function DataQualityView() {
|
|||||||
const [reports, setReports] = useState<ReportSummary[]>([])
|
const [reports, setReports] = useState<ReportSummary[]>([])
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [expandedCol, setExpandedCol] = useState<string | null>(null)
|
const [expandedCol, setExpandedCol] = useState<string | null>(null)
|
||||||
const [showGx, setShowGx] = useState(false)
|
const [expandedCheck, setExpandedCheck] = useState<string | null>(null)
|
||||||
const [showSoda, setShowSoda] = useState(false)
|
const [expandedDim, setExpandedDim] = useState<string | null>(null)
|
||||||
|
const [activeTool, setActiveTool] = useState<string>('great_expectations')
|
||||||
|
|
||||||
const loadMeta = useCallback(async () => {
|
const loadMeta = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -240,7 +283,7 @@ export function DataQualityView() {
|
|||||||
{caps && (
|
{caps && (
|
||||||
<div className="mt-3 grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="mt-3 grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<CapCard title="Maturity Engine" items={caps.maturity_dimensions.map((d) => d.label)} icon={Layers} />
|
<CapCard title="Maturity Engine" items={caps.maturity_dimensions.map((d) => d.label)} icon={Layers} />
|
||||||
<CapCard title="Data Quality Tools" items={['Great Expectations', 'Soda Core', 'Pandas Profiling']} icon={CheckCircle2} />
|
<CapCard title="Data Quality Tools" items={['Great Expectations', 'Soda Core', 'Deequ', 'Pandera', 'dbt', 'Monte Carlo', 'Affirm', 'Pandas']} icon={CheckCircle2} />
|
||||||
<CapCard title="Document Parsing" items={caps.tools.docling?.capabilities || ['PDF', 'PPTX', 'DOCX']} icon={FileText} />
|
<CapCard title="Document Parsing" items={caps.tools.docling?.capabilities || ['PDF', 'PPTX', 'DOCX']} icon={FileText} />
|
||||||
<CapCard title="File formats" items={[...caps.supported_data_formats.slice(0, 4), ...caps.supported_document_formats.slice(0, 3)]} icon={Upload} />
|
<CapCard title="File formats" items={[...caps.supported_data_formats.slice(0, 4), ...caps.supported_document_formats.slice(0, 3)]} icon={Upload} />
|
||||||
</div>
|
</div>
|
||||||
@@ -287,8 +330,8 @@ export function DataQualityView() {
|
|||||||
<StatCard label="Overall Score" value={`${assess.overall_score}`} sub="/100" accent />
|
<StatCard label="Overall Score" value={`${assess.overall_score}`} sub="/100" accent />
|
||||||
<StatCard label="Maturity Level" value={assess.maturity_level} sub={assess.maturity_description} />
|
<StatCard label="Maturity Level" value={assess.maturity_level} sub={assess.maturity_description} />
|
||||||
<StatCard label="Dataset" value={`${assess.rows.toLocaleString()}`} sub={`${assess.columns} columns`} />
|
<StatCard label="Dataset" value={`${assess.rows.toLocaleString()}`} sub={`${assess.columns} columns`} />
|
||||||
<StatCard label="Great Expectations" value={`${assess.checks_summary.great_expectations.passed}/${assess.checks_summary.great_expectations.total}`} sub="checks passed" />
|
<StatCard label="DQ Checks" value={String(assess.executive_summary?.checks_run || Object.values(assess.checks_summary).reduce((s, x) => s + (x.total || 0), 0))} sub="across 7 tools" />
|
||||||
<StatCard label="Soda Core" value={String(assess.checks_summary.soda_core.warnings)} sub="warnings" warn={assess.checks_summary.soda_core.warnings > 0} />
|
<StatCard label="Issues" value={String(assess.executive_summary?.issues_found ?? 0)} sub="failed/warn checks" warn={(assess.executive_summary?.issues_found ?? 0) > 0} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{assess.docling?.used && assess.docling.document_structure && (
|
{assess.docling?.used && assess.docling.document_structure && (
|
||||||
@@ -316,38 +359,69 @@ export function DataQualityView() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{assess.executive_summary && (
|
||||||
|
<section className="rounded-lg border border-docker/30 bg-docker/5 p-4">
|
||||||
|
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-docker">Executive Summary — Customer Report</h3>
|
||||||
|
<p className="text-[13px] font-semibold text-foreground">{assess.executive_summary.headline}</p>
|
||||||
|
<p className="mt-1 text-[11px] text-foreground-muted">{assess.executive_summary.dataset}</p>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-3 text-[10px]">
|
||||||
|
<span>{assess.executive_summary.checks_run} checks run</span>
|
||||||
|
<span className={assess.executive_summary.issues_found > 0 ? 'text-warning' : 'text-success'}>{assess.executive_summary.issues_found} issues found</span>
|
||||||
|
<span>{assess.executive_summary.flagged_columns} flagged columns</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 border-t border-border/50 pt-2 text-[11px] text-foreground-muted">
|
||||||
|
<strong className="text-foreground">Recommendation:</strong> {assess.executive_summary.recommendation}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<a href={assess.report_url} target="_blank" rel="noreferrer" className={cn('rounded-md px-3 py-1.5 text-[11px] font-medium', subTabActive)}>
|
<a href={assess.report_url} target="_blank" rel="noreferrer" className={cn('rounded-md px-3 py-1.5 text-[11px] font-medium', subTabActive)}>
|
||||||
Full HTML report ↗
|
Full HTML report ↗
|
||||||
</a>
|
</a>
|
||||||
<button type="button" onClick={() => setShowGx(!showGx)} className={cn('rounded-md px-3 py-1.5 text-[11px]', showGx ? subTabActive : subTabIdle)}>
|
<a href={assess.report_url + '/json'} target="_blank" rel="noreferrer" className={cn('rounded-md px-3 py-1.5 text-[11px]', subTabIdle)}>
|
||||||
GE checks ({assess.checks?.great_expectations.length || 0})
|
JSON export ↗
|
||||||
</button>
|
</a>
|
||||||
<button type="button" onClick={() => setShowSoda(!showSoda)} className={cn('rounded-md px-3 py-1.5 text-[11px]', showSoda ? subTabActive : subTabIdle)}>
|
|
||||||
Soda checks ({assess.checks?.soda_core.length || 0})
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showGx && assess.checks?.great_expectations && (
|
<section>
|
||||||
<CheckTable title="Great Expectations" rows={assess.checks.great_expectations.map((c) => ({
|
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">Data Quality Tools — click any row for error details</h3>
|
||||||
name: c.column ? `${c.expectation} [${c.column}]` : c.expectation,
|
<div className="mb-2 flex flex-wrap gap-1">
|
||||||
status: c.success ? 'pass' : 'fail',
|
{([
|
||||||
detail: c.result,
|
['great_expectations', 'Great Expectations'],
|
||||||
}))} />
|
['soda_core', 'Soda Core'],
|
||||||
)}
|
['deequ', 'AWS Deequ'],
|
||||||
{showSoda && assess.checks?.soda_core && (
|
['pandera', 'Pandera'],
|
||||||
<CheckTable title="Soda Core" rows={assess.checks.soda_core.map((c) => ({
|
['dbt_expectations', 'dbt Expectations'],
|
||||||
name: c.name,
|
['monte_carlo', 'Monte Carlo'],
|
||||||
status: c.outcome,
|
['affirm', 'Affirm'],
|
||||||
detail: `${c.check} — ${c.detail}`,
|
] as const).map(([key, label]) => {
|
||||||
}))} />
|
const items = assess.checks?.[key] || []
|
||||||
|
if (!items.length) return null
|
||||||
|
const summary = assess.checks_summary?.[key]
|
||||||
|
const badge = summary?.passed !== undefined ? `${summary.passed}/${summary.total}` : `${items.length}`
|
||||||
|
return (
|
||||||
|
<button key={key} type="button" onClick={() => setActiveTool(key)} className={cn('rounded-md px-2.5 py-1 text-[10px]', activeTool === key ? subTabActive : subTabIdle)}>
|
||||||
|
{label} ({badge})
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{assess.checks && (
|
||||||
|
<CheckTable
|
||||||
|
title={activeTool.replace(/_/g, ' ')}
|
||||||
|
checks={normalizeChecks(assess.checks[activeTool as keyof typeof assess.checks] || [])}
|
||||||
|
expandedId={expandedCheck}
|
||||||
|
onToggle={setExpandedCheck}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3 className="mb-3 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">6 Maturity Dimensions</h3>
|
<h3 className="mb-3 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">6 Maturity Dimensions</h3>
|
||||||
<div className="grid gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
<div className="grid gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
{assess.dimensions.map((d) => (
|
{assess.dimensions.map((d) => (
|
||||||
<DimensionCard key={d.id} dimension={d} />
|
<DimensionCard key={d.id} dimension={d} expanded={expandedDim === d.id} onToggle={() => setExpandedDim(expandedDim === d.id ? null : d.id)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -383,7 +457,7 @@ export function DataQualityView() {
|
|||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<p className="text-[12px] leading-relaxed text-foreground-muted">
|
<p className="text-[12px] leading-relaxed text-foreground-muted">
|
||||||
Docling extracts text, tables, images and document structure from PDF, PowerPoint, Word, Excel and images.
|
Docling extracts text, tables, images and document structure from PDF, PowerPoint, Word, Excel and images.
|
||||||
Resultaat: Markdown, HTML, JSON met pagina's, plaatjes, tabellen en outline.
|
Output: Markdown, HTML, JSON with pages, images, tables and document outline.
|
||||||
</p>
|
</p>
|
||||||
<UploadZone
|
<UploadZone
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -398,8 +472,8 @@ export function DataQualityView() {
|
|||||||
{parse && (
|
{parse && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6">
|
||||||
<StatCard label="Bestand" value={parse.filename.length > 20 ? parse.filename.slice(0, 18) + '…' : parse.filename} sub={parse.status} />
|
<StatCard label="File" value={parse.filename.length > 20 ? parse.filename.slice(0, 18) + '…' : parse.filename} sub={parse.status} />
|
||||||
<StatCard label="Verwerking" value={`${(parse.processing_time_sec || 0).toFixed(1)}s`} sub={`Formats: ${parse.formats_available.join(', ')}`} />
|
<StatCard label="Processing" value={`${(parse.processing_time_sec || 0).toFixed(1)}s`} sub={`Formats: ${parse.formats_available.join(', ')}`} />
|
||||||
<StatCard label="Pages" value={String(parse.document_structure?.pages ?? parse.stats.pages ?? 0)} icon={Layers} />
|
<StatCard label="Pages" value={String(parse.document_structure?.pages ?? parse.stats.pages ?? 0)} icon={Layers} />
|
||||||
<StatCard label="Images" value={String(parse.document_structure?.pictures ?? 0)} icon={Image} accent />
|
<StatCard label="Images" value={String(parse.document_structure?.pictures ?? 0)} icon={Image} accent />
|
||||||
<StatCard label="Tables" value={String(parse.document_structure?.tables ?? 0)} icon={Table2} />
|
<StatCard label="Tables" value={String(parse.document_structure?.tables ?? 0)} icon={Table2} />
|
||||||
@@ -498,7 +572,7 @@ function ImageGallery({ images, parseId }: { images: DocImage[]; parseId: string
|
|||||||
return (
|
return (
|
||||||
<section className="rounded-lg border border-border bg-surface-overlay/30 p-4">
|
<section className="rounded-lg border border-border bg-surface-overlay/30 p-4">
|
||||||
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">
|
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">
|
||||||
Images gedetecteerd ({images.length}) — no embedded export
|
Images detected ({images.length}) — no embedded export
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[11px] text-foreground-muted">Re-upload the document to extract images (embedded mode).</p>
|
<p className="text-[11px] text-foreground-muted">Re-upload the document to extract images (embedded mode).</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -508,7 +582,7 @@ function ImageGallery({ images, parseId }: { images: DocImage[]; parseId: string
|
|||||||
<section className="rounded-lg border border-border bg-surface-overlay/30 p-4">
|
<section className="rounded-lg border border-border bg-surface-overlay/30 p-4">
|
||||||
<h3 className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">
|
<h3 className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">
|
||||||
<Image className="h-4 w-4 text-docker" />
|
<Image className="h-4 w-4 text-docker" />
|
||||||
Images die Docling ziet ({available.length})
|
Images extracted by Docling ({available.length})
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||||
{available.map((img) => (
|
{available.map((img) => (
|
||||||
@@ -601,9 +675,13 @@ function DocStructurePanel({ structure, title }: { structure: DocStructure; titl
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DimensionCard({ dimension: d }: { dimension: Dimension }) {
|
function normalizeChecks(items: DqCheck[]): DqCheck[] {
|
||||||
|
return items.map((c, i) => ({ ...c, id: c.id || `${c.suite}_${i}` }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function DimensionCard({ dimension: d, expanded, onToggle }: { dimension: Dimension; expanded?: boolean; onToggle?: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-border bg-surface-overlay/40 p-3">
|
<button type="button" onClick={onToggle} className="w-full rounded-lg border border-border bg-surface-overlay/40 p-3 text-left transition-colors hover:border-docker/30">
|
||||||
<div className="mb-1 flex items-center justify-between">
|
<div className="mb-1 flex items-center justify-between">
|
||||||
<span className="text-[12px] font-semibold">{d.label}</span>
|
<span className="text-[12px] font-semibold">{d.label}</span>
|
||||||
<span className={cn('text-base font-bold', SCORE_COLOR(d.score))}>{d.score}</span>
|
<span className={cn('text-base font-bold', SCORE_COLOR(d.score))}>{d.score}</span>
|
||||||
@@ -617,10 +695,16 @@ function DimensionCard({ dimension: d }: { dimension: Dimension }) {
|
|||||||
<li key={f} className="flex gap-1"><span className="text-docker">▸</span>{f}</li>
|
<li key={f} className="flex gap-1"><span className="text-docker">▸</span>{f}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{d.recommended_actions && d.recommended_actions.length > 0 && (
|
{expanded && d.recommended_actions && d.recommended_actions.length > 0 && (
|
||||||
<p className="mt-2 border-t border-border pt-2 text-[9px] text-warning">→ {d.recommended_actions[0]}</p>
|
<div className="mt-2 border-t border-border pt-2">
|
||||||
)}
|
<p className="mb-1 text-[9px] font-semibold uppercase text-foreground-faint">Recommended actions</p>
|
||||||
|
{d.recommended_actions.map((a) => <p key={a} className="text-[9px] text-warning">→ {a}</p>)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{!expanded && d.recommended_actions && d.recommended_actions.length > 0 && (
|
||||||
|
<p className="mt-2 border-t border-border pt-2 text-[9px] text-foreground-faint">Click for {d.recommended_actions.length} recommended action(s)</p>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,27 +746,78 @@ function ColumnTable({ profiles, expandedCol, onToggle }: { profiles: ColumnProf
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CheckTable({ title, rows }: { title: string; rows: { name: string; status: string; detail: string }[] }) {
|
function checkLabel(c: DqCheck): string {
|
||||||
|
const base = c.expectation || c.name || c.metric || c.check || c.test || c.rule || c.monitor || 'check'
|
||||||
|
return c.column ? `${base} [${c.column}]` : base
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkStatus(c: DqCheck): string {
|
||||||
|
return c.status || (c.success === false ? 'fail' : c.success ? 'pass' : c.outcome || 'unknown')
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDetail(c: DqCheck): string {
|
||||||
|
return c.result || c.detail || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function CheckTable({ title, checks, expandedId, onToggle }: { title: string; checks: DqCheck[]; expandedId: string | null; onToggle: (id: string | null) => void }) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto rounded-lg border border-border">
|
<div className="overflow-x-auto rounded-lg border border-border">
|
||||||
<p className="border-b border-border bg-surface-overlay px-3 py-2 text-[11px] font-semibold">{title}</p>
|
<p className="border-b border-border bg-surface-overlay px-3 py-2 text-[11px] font-semibold capitalize">{title}</p>
|
||||||
<table className="w-full text-[10px]">
|
<table className="w-full text-[10px]">
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map((r, i) => (
|
{checks.map((c) => {
|
||||||
<tr key={i} className="border-t border-border">
|
const id = c.id || checkLabel(c)
|
||||||
|
const status = checkStatus(c)
|
||||||
|
const viol = c.violations
|
||||||
|
return (
|
||||||
|
<Fragment key={id}>
|
||||||
|
<tr className="cursor-pointer border-t border-border hover:bg-surface-overlay/50" onClick={() => onToggle(expandedId === id ? null : id)}>
|
||||||
<td className="px-3 py-1.5">
|
<td className="px-3 py-1.5">
|
||||||
<span className={cn('mr-2 rounded px-1.5 py-0.5 text-[9px] font-bold uppercase',
|
<span className={cn('mr-2 rounded px-1.5 py-0.5 text-[9px] font-bold uppercase',
|
||||||
r.status === 'pass' ? 'bg-success/20 text-success' : r.status === 'warn' ? 'bg-warning/20 text-warning' : 'bg-danger/20 text-danger')}>
|
status === 'pass' ? 'bg-success/20 text-success' : status === 'warn' ? 'bg-warning/20 text-warning' : 'bg-danger/20 text-danger')}>
|
||||||
{r.status}
|
{status}
|
||||||
</span>
|
</span>
|
||||||
{r.name}
|
{checkLabel(c)}
|
||||||
|
{viol?.violation_count ? <span className="ml-2 text-[9px] text-danger">{viol.violation_count} violations</span> : null}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-1.5 text-foreground-muted">{r.detail}</td>
|
<td className="px-3 py-1.5 text-foreground-muted">{checkDetail(c)}</td>
|
||||||
|
</tr>
|
||||||
|
{expandedId === id && (
|
||||||
|
<tr className="border-t border-border bg-surface-overlay/20">
|
||||||
|
<td colSpan={2} className="px-4 py-3 text-[10px]">
|
||||||
|
<p className="mb-2 font-semibold text-docker">Error location & samples</p>
|
||||||
|
{viol?.violation_count ? (
|
||||||
|
<>
|
||||||
|
<p className="text-foreground-muted">{viol.violation_count} rows affected ({viol.violation_pct}%){viol.location_hint ? ` · ${viol.location_hint}` : ''}</p>
|
||||||
|
{viol.sample_rows && viol.sample_rows.length > 0 && (
|
||||||
|
<div className="mt-2 overflow-x-auto rounded border border-border">
|
||||||
|
<table className="w-full text-[9px]">
|
||||||
|
<thead><tr className="bg-surface-overlay"><th className="px-2 py-1">Row #</th>{Object.keys(viol.sample_rows[0].values).map((k) => <th key={k} className="px-2 py-1">{k}</th>)}</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{viol.sample_rows.map((s) => (
|
||||||
|
<tr key={s.row_index} className="border-t border-border/50">
|
||||||
|
<td className="px-2 py-1 font-mono text-warning">{s.row_index}</td>
|
||||||
|
{Object.values(s.values).map((v, j) => <td key={j} className="max-w-[120px] truncate px-2 py-1">{v === null ? 'NULL' : String(v)}</td>)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-success">No row-level violations — check passed.</p>
|
||||||
|
)}
|
||||||
|
{c.meta && <pre className="mt-2 rounded bg-black/30 p-2 font-mono text-[9px]">{JSON.stringify(c.meta, null, 2)}</pre>}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function InfraQuickAccess({
|
|||||||
<div className="mb-2 flex items-center justify-between gap-2">
|
<div className="mb-2 flex items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-[9px] font-semibold uppercase tracking-wider text-foreground-muted">Infrastructure & Apps</h3>
|
<h3 className="text-[9px] font-semibold uppercase tracking-wider text-foreground-muted">Infrastructure & Apps</h3>
|
||||||
<p className="text-[8px] text-foreground-faint">Klik voor inspector · Shell kopieert SSH en opent live terminal · UI opent de applicatie</p>
|
<p className="text-[8px] text-foreground-faint">Click for inspector · Shell copies SSH and opens live terminal · UI opens the app</p>
|
||||||
</div>
|
</div>
|
||||||
{workload && (
|
{workload && (
|
||||||
<span className="shrink-0 font-mono text-[8px] text-foreground-faint">
|
<span className="shrink-0 font-mono text-[8px] text-foreground-faint">
|
||||||
|
|||||||
@@ -95,16 +95,16 @@ export function InspectorPanel({
|
|||||||
{!d && !agent && (
|
{!d && !agent && (
|
||||||
<div className="scrollbar-thin flex-1 space-y-3 overflow-y-auto p-3">
|
<div className="scrollbar-thin flex-1 space-y-3 overflow-y-auto p-3">
|
||||||
<div className="rounded-lg border border-docker/25 bg-docker-light/50 p-2.5 dark:bg-blue-500/10">
|
<div className="rounded-lg border border-docker/25 bg-docker-light/50 p-2.5 dark:bg-blue-500/10">
|
||||||
<p className="mb-1.5 text-[10px] font-semibold text-foreground">Snel starten</p>
|
<p className="mb-1.5 text-[10px] font-semibold text-foreground">Quick start</p>
|
||||||
<ol className="list-decimal space-y-1 pl-4 text-[10px] leading-relaxed text-foreground-muted">
|
<ol className="list-decimal space-y-1 pl-4 text-[10px] leading-relaxed text-foreground-muted">
|
||||||
<li>Select an <strong className="text-foreground">infrastructure card</strong> or topology node</li>
|
<li>Select an <strong className="text-foreground">infrastructure card</strong> or topology node</li>
|
||||||
<li>Klik <strong className="text-foreground">Shell</strong> voor SSH + live terminal output</li>
|
<li>Click <strong className="text-foreground">Shell</strong> for SSH + live terminal output</li>
|
||||||
<li>Klik <strong className="text-foreground">UI</strong> om Airflow, Trino, Kafka UI, etc. te openen</li>
|
<li>Click <strong className="text-foreground">UI</strong> to open Airflow, Trino, Kafka UI, etc.</li>
|
||||||
<li>Stel vragen via <strong className="text-foreground">Chat</strong> onderaan — agents zien de hele cluster</li>
|
<li>Ask questions via <strong className="text-foreground">Chat</strong> below — agents see the full cluster</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1.5 text-[9px] uppercase tracking-wider text-foreground-faint">Agents & domeinen</p>
|
<p className="mb-1.5 text-[9px] uppercase tracking-wider text-foreground-faint">Agents & domains</p>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{agents.filter((a) => !a.supervisor).map((a) => {
|
{agents.filter((a) => !a.supervisor).map((a) => {
|
||||||
const meta = getAgentMeta(a.id)
|
const meta = getAgentMeta(a.id)
|
||||||
@@ -201,7 +201,7 @@ export function InspectorPanel({
|
|||||||
{infra && (
|
{infra && (
|
||||||
<Button size="sm" variant="outline" onClick={runShell} disabled={busy}>
|
<Button size="sm" variant="outline" onClick={runShell} disabled={busy}>
|
||||||
<Terminal className="h-3 w-3" />
|
<Terminal className="h-3 w-3" />
|
||||||
{shellCopied ? 'SSH gekopieerd' : 'Shell'}
|
{shellCopied ? 'SSH copied' : 'Shell'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button size="sm" variant="outline" onClick={onProbe} disabled={busy}>
|
<Button size="sm" variant="outline" onClick={onProbe} disabled={busy}>
|
||||||
@@ -275,7 +275,7 @@ export function InspectorPanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={submit} className="flex gap-1 border-t border-border p-2">
|
<form onSubmit={submit} className="flex gap-1 border-t border-border p-2">
|
||||||
<Input value={input} onChange={(e) => setInput(e.target.value)} placeholder={`Vraag over ${d.label}…`} disabled={busy} className="text-xs" />
|
<Input value={input} onChange={(e) => setInput(e.target.value)} placeholder={`Ask about ${d.label}…`} disabled={busy} className="text-xs" />
|
||||||
<Button type="submit" size="sm" disabled={busy || !input.trim()}>Send</Button>
|
<Button type="submit" size="sm" disabled={busy || !input.trim()}>Send</Button>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ export function TerminalDock({ subjectId, subjectLabel, lines, busy, expanded, o
|
|||||||
<div className="scrollbar-thin min-h-0 flex-1 overflow-y-auto px-3 pb-2 font-mono text-[9px] leading-relaxed">
|
<div className="scrollbar-thin min-h-0 flex-1 overflow-y-auto px-3 pb-2 font-mono text-[9px] leading-relaxed">
|
||||||
{infra && (
|
{infra && (
|
||||||
<p className="mb-1 text-foreground-faint">
|
<p className="mb-1 text-foreground-faint">
|
||||||
<span className="text-docker">$</span> {infra.ssh} <span className="text-foreground-faint/70">(gekopieerd bij Shell-knop)</span>
|
<span className="text-docker">$</span> {infra.ssh} <span className="text-foreground-faint/70">(copied via Shell button)</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{lines.length === 0 && (
|
{lines.length === 0 && (
|
||||||
<p className="py-4 text-center text-foreground-faint">Selecteer een node of agent · klik Shell of Probe om output te zien</p>
|
<p className="py-4 text-center text-foreground-faint">Select a node or agent · click Shell or Probe to see output</p>
|
||||||
)}
|
)}
|
||||||
{lines.map((line) => (
|
{lines.map((line) => (
|
||||||
<div key={line.id} className={LEVEL[line.level] || 'text-foreground-muted'}>
|
<div key={line.id} className={LEVEL[line.level] || 'text-foreground-muted'}>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function WorkbenchPanel({ mode, agent, lines, busy, onSendPrompt }: Props
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}}
|
}}
|
||||||
className="flex h-2.5 shrink-0 cursor-ns-resize items-center justify-center border-b border-border/50 bg-surface-overlay/60 hover:bg-docker/10"
|
className="flex h-2.5 shrink-0 cursor-ns-resize items-center justify-center border-b border-border/50 bg-surface-overlay/60 hover:bg-docker/10"
|
||||||
title="Sleep omhoog/omlaag om grootte aan te passen"
|
title="Drag up/down to resize"
|
||||||
>
|
>
|
||||||
<GripHorizontal className="h-3 w-3 text-foreground-faint" />
|
<GripHorizontal className="h-3 w-3 text-foreground-faint" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user