Fix remaining Dutch string in DataQualityView

This commit is contained in:
mo
2026-06-25 10:00:07 +00:00
parent 41a8a3b18e
commit ff2c31a6b5
10 changed files with 223 additions and 88 deletions
+15 -15
View File
@@ -105,9 +105,9 @@ AGENTS = [
"motto": "Guardian of every row",
"capabilities": ["PostgreSQL", "MySQL", "MongoDB", "Cassandra", "Neo4j"],
"suggested_prompts": [
"Hoeveel data zit er in de databases?",
"Wat staat er in PostgreSQL sales_orders?",
"MongoDB supplychain overzicht",
"How much data is in the databases?",
"What is in PostgreSQL sales_orders?",
"MongoDB supplychain overview",
],
},
{
@@ -332,21 +332,21 @@ async def ask_llm(
log: Any | None = None,
) -> str | None:
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.
Specialisatie: {agent['role']}.
Motto: {agent.get('motto', '')}
system = f"""You are {agent['name']}, an autonomous ops agent in the Dell ATC data lab.
Specialization: {agent['role']}.
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:
- Antwoord in dezelfde taal als de gebruiker (Nederlands of Engels).
- Je hebt volledige zicht op de HELE cluster: alle VMs, zones, connectors, GPU, Hadoop, ObjectScale en Command Center.
- Gebruik ALLEEN de live data hieronder — verzin geen hosts, poorten, cijfers of connector namen.
- Gebruik exact de container/connector namen uit de data (bijv. mysql-hr-connector, niet "Debezium").
- Als iets DOWN of 0 GB is, zeg dat eerlijk.
- Kort en behulpzaam (max ~10 zinnen); bullet lists mogen als het overzicht helpt.
Rules:
- Always respond in English.
- You have full visibility into the entire cluster: all VMs, zones, connectors, GPU, Hadoop, ObjectScale and Command Center.
- Use ONLY the live data below — do not invent hosts, ports, numbers or connector names.
- Use exact container/connector names from the data (e.g. mysql-hr-connector, not "Debezium").
- If something is DOWN or 0 GB, say so honestly.
- 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}
"""
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">
{items.length === 0 && (
<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>
)}
{items.map((e) => {
+1 -1
View File
@@ -53,7 +53,7 @@ export function ChatDrawer({
Chat & Activity
{unread > 0 && (
<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>
)}
{approvals.length > 0 && (
+3 -3
View File
@@ -21,9 +21,9 @@ export function CommandBar({ busy, selectedAgent, onSubmit }: Props) {
}
const suggestions = selectedAgent?.suggested_prompts?.slice(0, 3) || [
'Hoeveel data zit er in de databases?',
'Wat staat er in PostgreSQL?',
'MongoDB supplychain overzicht',
'How much data is in the databases?',
'What is in PostgreSQL?',
'MongoDB supplychain overview',
]
return (
+1 -1
View File
@@ -58,7 +58,7 @@ export function CommsPanel({ messages, agents, selectedAgent, busy }: Props) {
{busy && (
<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>Agent verzamelt cluster-data en vraagt Llama 70B verwacht ~3090 sec</span>
<span>Agent gathering cluster data and querying Llama 70B expect ~3090 sec</span>
</div>
)}
<div ref={bottomRef} />
+191 -56
View File
@@ -37,8 +37,36 @@ type ColumnProfile = {
top_values?: { value: string; count: number }[]
}
type GxCheck = { suite: string; expectation: string; success: boolean; result: string; column?: string }
type SodaCheck = { suite: string; name: string; check: string; outcome: string; detail: string }
type Violations = {
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 = {
pages: number
@@ -67,10 +95,24 @@ type AssessResult = {
dimensions: Dimension[]
column_profiles: ColumnProfile[]
action_items: { priority: string; dimension: string; score: number; action: string }[]
checks?: { great_expectations: GxCheck[]; soda_core: SodaCheck[] }
checks_summary: {
great_expectations: { total: number; passed: number }
soda_core: { total: number; warnings: number }
checks?: {
great_expectations: GxCheck[]
soda_core: SodaCheck[]
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[] }
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 [error, setError] = useState<string | null>(null)
const [expandedCol, setExpandedCol] = useState<string | null>(null)
const [showGx, setShowGx] = useState(false)
const [showSoda, setShowSoda] = useState(false)
const [expandedCheck, setExpandedCheck] = useState<string | null>(null)
const [expandedDim, setExpandedDim] = useState<string | null>(null)
const [activeTool, setActiveTool] = useState<string>('great_expectations')
const loadMeta = useCallback(async () => {
try {
@@ -240,7 +283,7 @@ export function DataQualityView() {
{caps && (
<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="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="File formats" items={[...caps.supported_data_formats.slice(0, 4), ...caps.supported_document_formats.slice(0, 3)]} icon={Upload} />
</div>
@@ -287,8 +330,8 @@ export function DataQualityView() {
<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="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="Soda Core" value={String(assess.checks_summary.soda_core.warnings)} sub="warnings" warn={assess.checks_summary.soda_core.warnings > 0} />
<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="Issues" value={String(assess.executive_summary?.issues_found ?? 0)} sub="failed/warn checks" warn={(assess.executive_summary?.issues_found ?? 0) > 0} />
</div>
{assess.docling?.used && assess.docling.document_structure && (
@@ -316,38 +359,69 @@ export function DataQualityView() {
</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">
<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
</a>
<button type="button" onClick={() => setShowGx(!showGx)} className={cn('rounded-md px-3 py-1.5 text-[11px]', showGx ? subTabActive : subTabIdle)}>
GE checks ({assess.checks?.great_expectations.length || 0})
</button>
<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>
<a href={assess.report_url + '/json'} target="_blank" rel="noreferrer" className={cn('rounded-md px-3 py-1.5 text-[11px]', subTabIdle)}>
JSON export
</a>
</div>
{showGx && assess.checks?.great_expectations && (
<CheckTable title="Great Expectations" rows={assess.checks.great_expectations.map((c) => ({
name: c.column ? `${c.expectation} [${c.column}]` : c.expectation,
status: c.success ? 'pass' : 'fail',
detail: c.result,
}))} />
)}
{showSoda && assess.checks?.soda_core && (
<CheckTable title="Soda Core" rows={assess.checks.soda_core.map((c) => ({
name: c.name,
status: c.outcome,
detail: `${c.check}${c.detail}`,
}))} />
)}
<section>
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">Data Quality Tools click any row for error details</h3>
<div className="mb-2 flex flex-wrap gap-1">
{([
['great_expectations', 'Great Expectations'],
['soda_core', 'Soda Core'],
['deequ', 'AWS Deequ'],
['pandera', 'Pandera'],
['dbt_expectations', 'dbt Expectations'],
['monte_carlo', 'Monte Carlo'],
['affirm', 'Affirm'],
] 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>
<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">
{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>
</section>
@@ -383,7 +457,7 @@ export function DataQualityView() {
<div className="space-y-5">
<p className="text-[12px] leading-relaxed text-foreground-muted">
Docling extracts text, tables, images and document structure from PDF, PowerPoint, Word, Excel and images.
Resultaat: Markdown, HTML, JSON met pagina&apos;s, plaatjes, tabellen en outline.
Output: Markdown, HTML, JSON with pages, images, tables and document outline.
</p>
<UploadZone
loading={loading}
@@ -398,8 +472,8 @@ export function DataQualityView() {
{parse && (
<div className="space-y-4">
<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="Verwerking" value={`${(parse.processing_time_sec || 0).toFixed(1)}s`} sub={`Formats: ${parse.formats_available.join(', ')}`} />
<StatCard label="File" value={parse.filename.length > 20 ? parse.filename.slice(0, 18) + '…' : parse.filename} sub={parse.status} />
<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="Images" value={String(parse.document_structure?.pictures ?? 0)} icon={Image} accent />
<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 (
<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">
Images gedetecteerd ({images.length}) no embedded export
Images detected ({images.length}) no embedded export
</h3>
<p className="text-[11px] text-foreground-muted">Re-upload the document to extract images (embedded mode).</p>
</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">
<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" />
Images die Docling ziet ({available.length})
Images extracted by Docling ({available.length})
</h3>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{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 (
<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">
<span className="text-[12px] font-semibold">{d.label}</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>
))}
</ul>
{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>
{expanded && d.recommended_actions && d.recommended_actions.length > 0 && (
<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,24 +746,75 @@ 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 (
<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]">
<tbody>
{rows.map((r, i) => (
<tr key={i} className="border-t border-border">
<td className="px-3 py-1.5">
<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')}>
{r.status}
</span>
{r.name}
</td>
<td className="px-3 py-1.5 text-foreground-muted">{r.detail}</td>
</tr>
))}
{checks.map((c) => {
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">
<span className={cn('mr-2 rounded px-1.5 py-0.5 text-[9px] font-bold uppercase',
status === 'pass' ? 'bg-success/20 text-success' : status === 'warn' ? 'bg-warning/20 text-warning' : 'bg-danger/20 text-danger')}>
{status}
</span>
{checkLabel(c)}
{viol?.violation_count ? <span className="ml-2 text-[9px] text-danger">{viol.violation_count} violations</span> : null}
</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 &amp; 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>
))}
</tbody>
</table>
</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>
<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>
{workload && (
<span className="shrink-0 font-mono text-[8px] text-foreground-faint">
@@ -95,16 +95,16 @@ export function InspectorPanel({
{!d && !agent && (
<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">
<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">
<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>Klik <strong className="text-foreground">UI</strong> om Airflow, Trino, Kafka UI, etc. te openen</li>
<li>Stel vragen via <strong className="text-foreground">Chat</strong> onderaan agents zien de hele cluster</li>
<li>Click <strong className="text-foreground">Shell</strong> for SSH + live terminal output</li>
<li>Click <strong className="text-foreground">UI</strong> to open Airflow, Trino, Kafka UI, etc.</li>
<li>Ask questions via <strong className="text-foreground">Chat</strong> below agents see the full cluster</li>
</ol>
</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">
{agents.filter((a) => !a.supervisor).map((a) => {
const meta = getAgentMeta(a.id)
@@ -201,7 +201,7 @@ export function InspectorPanel({
{infra && (
<Button size="sm" variant="outline" onClick={runShell} disabled={busy}>
<Terminal className="h-3 w-3" />
{shellCopied ? 'SSH gekopieerd' : 'Shell'}
{shellCopied ? 'SSH copied' : 'Shell'}
</Button>
)}
<Button size="sm" variant="outline" onClick={onProbe} disabled={busy}>
@@ -275,7 +275,7 @@ export function InspectorPanel({
)}
</div>
<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>
</form>
</>
+2 -2
View File
@@ -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">
{infra && (
<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>
)}
{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) => (
<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()
}}
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" />
</div>