This is an automated email from the ASF dual-hosted git repository.

luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit ccf670e158a95b808554da5e09911bf3b38aea34
Author: Luigi De Masi <[email protected]>
AuthorDate: Wed Oct 14 22:31:10 2026 +0200

    CAMEL-25669: Support image input and audit previews in JBang
    
    Accept explicit image paths, render bounded audit previews, and preserve 
captured audit text as literal evidence. Keep payloads out of normal output and 
report actionable export failures.
    
    Co-authored-by: OpenAI Codex <[email protected]>
    Signed-off-by: Luigi De Masi <[email protected]>
---
 .../apache/camel/catalog/docs/semantic-openai.adoc |  27 ++++
 .../src/main/docs/semantic-openai.adoc             |  27 ++++
 design/semantic-audit.adoc                         |   5 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   4 +-
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |  13 +-
 .../jbang-commands/camel-jbang-semantic-audit.adoc |   1 +
 .../jbang-commands/camel-jbang-semantic-eval.adoc  |   4 +-
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../jbang/core/commands/action/SemanticAudit.java  |  38 +++++-
 .../core/commands/action/SemanticEvaluate.java     |   8 +-
 .../jbang/core/common/SemanticImagePreview.java    | 117 ++++++++++++++++++
 .../dsl/jbang/core/common/SemanticMediaInput.java  | 134 ++++++++++++++++++++
 .../jbang/core/common/SemanticResultFormatter.java |  13 +-
 .../core/commands/action/SemanticCommandTest.java  |  52 ++++++++
 .../jbang/core/common/SemanticMediaInputTest.java  |  96 +++++++++++++++
 .../core/common/SemanticResultFormatterTest.java   |  10 ++
 .../jbang/core/commands/tui/SemanticAuditView.java |  50 +++++---
 .../core/commands/tui/SemanticEvaluationPopup.java |   9 +-
 .../jbang/core/commands/tui/SemanticImageView.java |  91 ++++++++++++++
 .../core/commands/tui/SemanticPlayground.java      |  47 +++++--
 .../core/commands/tui/SemanticResultView.java      |   4 +
 .../src/main/resources/tui/help/semantic.md        |  10 ++
 .../core/commands/tui/SemanticImageViewTest.java   | 137 +++++++++++++++++++++
 23 files changed, 857 insertions(+), 42 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-openai.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-openai.adoc
index 7ad4b3546904..40d8325df015 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-openai.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-openai.adoc
@@ -160,6 +160,33 @@ Refusal raises `OpenAIDecisionRefusalException`. Invalid 
or inconsistent provide
 with the route's normal `onException` policy. Unsupported input and invalid 
declarations fail
 validation; infrastructure failure is never mapped to false or a score.
 
+== Audit, CLI and TUI
+
+Audit records classify input as text, image, text plus images, structured or 
unsupported even
+when payload capture is disabled; bounded classification may report unknown. 
The optional `inputType` audit extension is projected only
+to backends that declare support. It contains no image payload and is not 
added to monitoring events.
+
+Image payload capture follows the existing per-expert opt-in, redactor and 
`maxChars` budget.
+An oversized image retains a bounded descriptor with 
`dataOmitted=snapshot_limit` when that
+fits the budget. A descriptor is not a replayable image. Captured content 
remains untrusted data.
+
+`camel semantic eval` accepts `--body=image:/path/picture.png` for a named 
definition, or
+`--input=image:/path/picture.png` for a direct expert call. Use `json:` for an 
ordered envelope.
+The TUI playground accepts `image:/path/picture.png` and JSON envelopes when 
the expert advertises
+image input. Files are read only on an explicit evaluation request.
+
+The TUI Audit inspector (`u`) renders bounded inline colour previews from 
retained image data.
+The CLI renders a plain-terminal preview. Decoding uses locally available 
ImageIO readers;
+a missing decoder (for example WebP on a stock JDK), invalid data or preview 
resource limit
+shows an explicit unavailable reason. Previews are limited to 64 pixels per 
dimension and the
+first image frame, with transparency on white; they do not alter the data 
submitted to the expert.
+
+Normal command output, including `--json`, replaces image base64 with 
metadata. Use an explicit
+`--output=audit.json` on `camel semantic audit` to write the full returned 
data to a new file;
+case/bulk exports still require `--include-input` to request captured input. 
Existing files are
+never overwritten. Audit drafts retain captured images for an explicit Run 
without putting
+base64 into the editor. Omitted/redacted images cannot be reconstructed by the 
playground.
+
 == Opt-in API smoke tests
 
 `camel-openai` includes `OpenAIDecisionsExternalServiceIT` for native text 
batches and mixed
diff --git 
a/components/camel-ai/camel-semantic-openai/src/main/docs/semantic-openai.adoc 
b/components/camel-ai/camel-semantic-openai/src/main/docs/semantic-openai.adoc
index 7ad4b3546904..40d8325df015 100644
--- 
a/components/camel-ai/camel-semantic-openai/src/main/docs/semantic-openai.adoc
+++ 
b/components/camel-ai/camel-semantic-openai/src/main/docs/semantic-openai.adoc
@@ -160,6 +160,33 @@ Refusal raises `OpenAIDecisionRefusalException`. Invalid 
or inconsistent provide
 with the route's normal `onException` policy. Unsupported input and invalid 
declarations fail
 validation; infrastructure failure is never mapped to false or a score.
 
+== Audit, CLI and TUI
+
+Audit records classify input as text, image, text plus images, structured or 
unsupported even
+when payload capture is disabled; bounded classification may report unknown. 
The optional `inputType` audit extension is projected only
+to backends that declare support. It contains no image payload and is not 
added to monitoring events.
+
+Image payload capture follows the existing per-expert opt-in, redactor and 
`maxChars` budget.
+An oversized image retains a bounded descriptor with 
`dataOmitted=snapshot_limit` when that
+fits the budget. A descriptor is not a replayable image. Captured content 
remains untrusted data.
+
+`camel semantic eval` accepts `--body=image:/path/picture.png` for a named 
definition, or
+`--input=image:/path/picture.png` for a direct expert call. Use `json:` for an 
ordered envelope.
+The TUI playground accepts `image:/path/picture.png` and JSON envelopes when 
the expert advertises
+image input. Files are read only on an explicit evaluation request.
+
+The TUI Audit inspector (`u`) renders bounded inline colour previews from 
retained image data.
+The CLI renders a plain-terminal preview. Decoding uses locally available 
ImageIO readers;
+a missing decoder (for example WebP on a stock JDK), invalid data or preview 
resource limit
+shows an explicit unavailable reason. Previews are limited to 64 pixels per 
dimension and the
+first image frame, with transparency on white; they do not alter the data 
submitted to the expert.
+
+Normal command output, including `--json`, replaces image base64 with 
metadata. Use an explicit
+`--output=audit.json` on `camel semantic audit` to write the full returned 
data to a new file;
+case/bulk exports still require `--include-input` to request captured input. 
Existing files are
+never overwritten. Audit drafts retain captured images for an explicit Run 
without putting
+base64 into the editor. Omitted/redacted images cannot be reconstructed by the 
playground.
+
 == Opt-in API smoke tests
 
 `camel-openai` includes `OpenAIDecisionsExternalServiceIT` for native text 
batches and mixed
diff --git a/design/semantic-audit.adoc b/design/semantic-audit.adoc
index 44ef1d5a7e3e..ee569eac24f0 100644
--- a/design/semantic-audit.adoc
+++ b/design/semantic-audit.adoc
@@ -88,6 +88,11 @@ capture remains opt-in, bounded and redacted. An image too 
large for the configu
 may be replaced by a bounded descriptor (`type=image`, optional mediaType, 
dataOmitted=snapshot_limit).
 The redactor still processes that detached snapshot. A descriptor cannot 
recreate missing data.
 
+Audit displays label input type even when payload capture is disabled. TUI 
image decoding occurs
+off the render thread and uses only retained bytes, with bounded thumbnails 
and an explicit
+unavailable state. Normal CLI human/JSON display omits base64; explicit file 
exports can retain
+requested captured input. No historical URL is fetched and browsing never 
invokes an expert.
+
 == Read-only access
 
 The reader SPI has no evidence-specific limit on getAll. Explicit console 
callers limit requests to
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index e535071478c6..01548ec434a0 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -744,4 +744,6 @@ for every expert before any provider performs inference. A 
shared validation fai
 expert's group as invalid input in the audit.
 
 Semantic audit backends can opt into the additive `inputType` extension. 
Readers that do not
-support it keep their existing projection.
+support it keep their existing projection. The `camel semantic audit --json` 
display omits image
+base64 and reports metadata instead. Use `--output=<new-file>` to export the 
full returned JSON,
+with `--include-input` for case/bulk requests that need captured input. 
Existing files are not overwritten.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index d13bb945ff69..9e5ab04496cf 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -536,10 +536,21 @@ and `Enter` to open one. `Esc` returns to the previous 
record. Association uses
 a shared breadcrumb does not establish causality or prove that the case is 
complete. Missing records
 and unsupported case retrieval remain explicit.
 
+The Input column and Input type detail distinguish text, images and mixed 
input, including when
+payload capture is disabled. Older records show that the input type was not 
recorded.
+For retained images, `u` renders an inline colour preview without printing 
base64. Decoding happens
+in the background and uses local image readers; unavailable/corrupt/oversized 
images show a reason.
+No remote URL is fetched. The preview uses the first frame and at most 64 
pixels per dimension.
+
 When input capture is enabled for an expert, `u` reveals the permitted stored 
input or its omission
 reason. Input capture is disabled by default; configure it in the semantic 
audit DSL. Only the
 selected state is captured, subject to its configured size limit and optional 
redactor. `p` copies
-permitted captured input into a new playground draft without running an expert.
+permitted captured input into a new playground draft without running an 
expert. Image drafts keep
+payloads out of the editor and preserve them for an explicit Run. A 
capture-limit descriptor is
+not a replayable image. Image-capable playgrounds accept 
`image:/path/picture.png` or a JSON media
+envelope; the file is read only when Run is requested. Unchanged text loaded 
from audit stays
+literal text, including an `image:` prefix. Definition samples also accept
+`"body":"image:/path/picture.png"`.
 
 The latest page refreshes every second; `Space` pauses or resumes polling. 
Older pages stay fixed
 until explicitly refreshed. Filters use exact values; `definition` and 
`operation` apply to all retained history.
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-audit.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-audit.adoc
index 631ce46266f9..58188160ae3c 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-audit.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-audit.adoc
@@ -34,6 +34,7 @@ camel semantic audit [options]
 | `--limit` | Maximum records per page (1-200) | 50 | int
 | `--max-records` | Case/export retrieval budget (1-1000) | 200 | int
 | `--operation` | Filter by recorded expert operation |  | String
+| `--output` | Write the full JSON response to a new file, including requested 
captured image data |  | Path
 | `--result-label` | Exact Classification member; requires 
--result-type=CLASSIFICATION |  | String
 | `--result-type` | Captured result type: BOOLEAN, CHOICE, SCORE or 
CLASSIFICATION |  | String
 | `--result-value` | Exact Boolean, Choice or numeric Score value; requires 
--result-type |  | String
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc
index 46a10dfd9c03..4415aef5c4bf 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc
@@ -19,12 +19,12 @@ camel semantic eval [options]
 [cols="2,5,1,2",options="header"]
 |===
 | Option | Description | Default | Type
-| `--body` | Sample message body (text, file:path or json:value) |  | String
+| `--body` | Sample body (text, file:path, image:path or json:value) |  | 
String
 | `--evaluation` | Named semantic definition to evaluate |  | String
 | `--evaluation-timeout` | Provider evaluation timeout in milliseconds 
(1-50000) | 50000 | long
 | `--expert` | Expert to call directly |  | String
 | `--header` | Sample header (key=value; use json: for a typed value) |  | List
-| `--input` | Direct expert input (text, file:path or json:value) |  | String
+| `--input` | Direct input (text, file:path, image:path or json:value) |  | 
String
 | `--json` | Output a single JSON document |  | boolean
 | `--operation` | Expert operation to call directly |  | String
 | `--parameter` | Expert parameter (key=value; use json: for a typed value) |  
| List
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index ee61341736b4..331d29b32497 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -30,7 +30,7 @@
     { "name": "run", "fullName": "run", "description": "Run as local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Run", 
"options": [ { "names": "--background", "description": "Run in the background", 
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To wait for run in background to startup 
successfully, before returning", "defaultValue": "true", "javaType": "boolean", 
"type": "boolean" }, { [...]
     { "name": "sbom", "fullName": "sbom", "description": "Generate a CycloneDX 
or SPDX SBOM for a specific project", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.SBOMGenerator", "options": [ { 
"names": "--build-property", "description": "Maven build properties, ex. 
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, { 
"names": "--camel-spring-boot-version", "description": "Camel version to use 
with Spring Boot", "javaType": "java.lang.String", "type" [...]
     { "name": "script", "fullName": "script", "description": "Run Camel 
integration as shell script for terminal scripting", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Script", "options": [ { "names": 
"--logging", "description": "Can be used to turn on logging (logs to file in 
<user home>\/.camel directory)", "defaultValue": "false", "javaType": 
"boolean", "type": "boolean" }, { "names": "--logging-level", "description": 
"Logging level (ERROR, WARN, INFO, DEBUG, TRACE)", "d [...]
-    { "name": "semantic", "fullName": "semantic", "description": "List 
semantic definitions and expert contracts", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelSemantic", "options": [ { 
"names": "--expert", "description": "Show the operations and parameter contract 
of this expert", "javaType": "java.lang.String", "type": "string" }, { "names": 
"--json", "description": "Output a single JSON document", "javaType": 
"boolean", "type": "boolean" }, { "names": "--timeou [...]
+    { "name": "semantic", "fullName": "semantic", "description": "List 
semantic definitions and expert contracts", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelSemantic", "options": [ { 
"names": "--expert", "description": "Show the operations and parameter contract 
of this expert", "javaType": "java.lang.String", "type": "string" }, { "names": 
"--json", "description": "Output a single JSON document", "javaType": 
"boolean", "type": "boolean" }, { "names": "--timeou [...]
     { "name": "shell", "fullName": "shell", "description": "Interactive Camel 
CLI shell.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Shell", 
"options": [ { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", "type": "boolean" } ] },
     { "name": "stop", "fullName": "stop", "description": "Shuts down running 
Camel integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.StopProcess", "options": [ { 
"names": "--kill", "description": "To force killing the process (SIGKILL)", 
"javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", 
"description": "Display the help and sub-commands", "javaType": "boolean", 
"type": "boolean" } ] },
     { "name": "top", "fullName": "top", "description": "Top status of Camel 
integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.CamelTop", "options": [ { 
"names": "--watch", "description": "Execute periodically and showing output 
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "context", 
"fullName": "top  [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticAudit.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticAudit.java
index e6bf4ac0de66..79465f060a01 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticAudit.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticAudit.java
@@ -16,6 +16,13 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.action;
 
+import java.io.IOException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileSystemException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 import java.time.Instant;
 import java.time.format.DateTimeParseException;
 import java.util.List;
@@ -29,6 +36,8 @@ import com.github.freva.asciitable.ColumnData;
 import com.github.freva.asciitable.HorizontalAlign;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.common.SemanticAuditFilters;
+import org.apache.camel.dsl.jbang.core.common.SemanticImagePreview;
+import org.apache.camel.dsl.jbang.core.common.SemanticMediaInput;
 import org.apache.camel.dsl.jbang.core.common.SemanticResultFormatter;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -112,6 +121,10 @@ public class SemanticAudit extends SemanticActionCommand {
     @CommandLine.Option(names = "--include-input", description = "Include 
untrusted captured input in an explicit case/export")
     boolean includeInput;
 
+    @CommandLine.Option(names = "--output",
+                        description = "Write the full JSON response to a new 
file, including requested captured image data")
+    Path output;
+
     public SemanticAudit(CamelJBangMain main) {
         super(main);
     }
@@ -205,12 +218,31 @@ public class SemanticAudit extends SemanticActionCommand {
         if (Boolean.TRUE.equals(response.get("cursorExpired"))) {
             return error(1, "Audit cursor expired; start again without 
--cursor", response);
         }
-        return super.printResponse(response);
+        if (output != null) {
+            try {
+                Files.writeString(output, response.toJson(), 
StandardOpenOption.CREATE_NEW);
+                printer().println("Audit exported to " + displayText(output));
+                return 0;
+            } catch (IOException e) {
+                String reason = e instanceof FileAlreadyExistsException ? 
"file already exists"
+                        : e instanceof AccessDeniedException ? "permission 
denied"
+                        : e instanceof FileSystemException failure && 
failure.getReason() != null
+                                ? displayText(failure.getReason())
+                        : "I/O failure (" + e.getClass().getSimpleName() + ")";
+                return error(1, "Cannot create audit export file: " + reason, 
null);
+            }
+        }
+        return super.printResponse(json ? (JsonObject) 
SemanticMediaInput.withoutImageData(response) : response);
     }
 
     private void renderAuditField(String key, Object value, String indent) {
         if ("input".equals(key)) {
-            renderInput("Input", value, indent);
+            renderInput("Input", SemanticMediaInput.withoutImageData(value), 
indent);
+            for (var preview : SemanticImagePreview.prepare(value)) {
+                preview.textLines().forEach(line -> printer().println(indent + 
line));
+            }
+        } else if ("inputType".equals(key)) {
+            renderField("Input type", SemanticMediaInput.typeLabel(value), 
indent);
         } else {
             renderField(fieldLabel(key), value, indent);
         }
@@ -266,6 +298,8 @@ public class SemanticAudit extends SemanticActionCommand {
                     column("EVENT ID", "eventId"), column("TIMESTAMP", 
"timestamp"),
                     column("EXECUTING ROUTE", "executingRouteId"),
                     column("EVALUATION", "definition"), column("EXPERT", 
"expert"),
+                    new 
Column().header("INPUT").dataAlign(HorizontalAlign.LEFT)
+                            .with((JsonObject row) -> 
SemanticMediaInput.typeLabel(row.get("inputType"))),
                     new 
Column().header("RESULT").dataAlign(HorizontalAlign.LEFT)
                             .with((JsonObject row) -> 
SemanticResultFormatter.audit(row).compact()),
                     column("EXECUTION", "status"), column("BREADCRUMB ID", 
"breadcrumbId"))));
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticEvaluate.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticEvaluate.java
index 481aa5f9eb13..94ddf41c8e21 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticEvaluate.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticEvaluate.java
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectReader;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.SemanticMediaInput;
 import org.apache.camel.util.json.JsonObject;
 import picocli.CommandLine;
 
@@ -43,7 +44,7 @@ public class SemanticEvaluate extends SemanticActionCommand {
     @CommandLine.Option(names = "--evaluation", description = "Named semantic 
definition to evaluate")
     String evaluation;
 
-    @CommandLine.Option(names = "--body", description = "Sample message body 
(text, file:path or json:value)")
+    @CommandLine.Option(names = "--body", description = "Sample body (text, 
file:path, image:path or json:value)")
     String body;
 
     @CommandLine.Option(names = "--header", description = "Sample header 
(key=value; use json: for a typed value)")
@@ -59,7 +60,7 @@ public class SemanticEvaluate extends SemanticActionCommand {
     @CommandLine.Option(names = "--operation", description = "Expert operation 
to call directly")
     String operation;
 
-    @CommandLine.Option(names = "--input", description = "Direct expert input 
(text, file:path or json:value)")
+    @CommandLine.Option(names = "--input", description = "Direct input (text, 
file:path, image:path or json:value)")
     String input;
 
     @CommandLine.Option(names = "--parameter", description = "Expert parameter 
(key=value; use json: for a typed value)")
@@ -146,6 +147,9 @@ public class SemanticEvaluate extends SemanticActionCommand 
{
     }
 
     private static Object value(String value, boolean allowFile) {
+        if (allowFile && value.startsWith("image:")) {
+            return SemanticMediaInput.readImage(value);
+        }
         if (value.startsWith("json:")) {
             try {
                 return JSON_VALUE.readValue(value.substring(5));
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/SemanticImagePreview.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/SemanticImagePreview.java
new file mode 100644
index 000000000000..1307ba4a33a3
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/SemanticImagePreview.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+import javax.imageio.ImageIO;
+import javax.imageio.stream.MemoryCacheImageInputStream;
+
+/** Bounded local thumbnail decoding. Never fetches a URL, opens a viewer, or 
writes image data to disk. */
+public record SemanticImagePreview(String description, List<List<Integer>> 
pixels) {
+    public SemanticImagePreview {
+        pixels = pixels.stream().map(List::copyOf).toList();
+    }
+
+    public static List<SemanticImagePreview> prepare(Object input) {
+        List<SemanticImagePreview> previews = new ArrayList<>();
+        for (Object item : SemanticMediaInput.parts(input)) {
+            if (item instanceof Map<?, ?> part && 
"image".equals(part.get("type"))) {
+                previews.add(decode(part));
+            }
+        }
+        return List.copyOf(previews);
+    }
+
+    private static SemanticImagePreview decode(Map<?, ?> part) {
+        var metadata = SemanticMediaInput.imageDescription(part);
+        String label = "Image (" + metadata.getOrDefault("mediaType", "format 
not recorded") + ")";
+        if (!(part.get("data") instanceof String data)) {
+            return unavailable(label, "payload was not retained");
+        }
+        int start = SemanticMediaInput.imageDataOffset(data);
+        if (start < 0 || data.length() > 1_000_000) {
+            return unavailable(label, "invalid image or preview byte limit");
+        }
+        try (var stream = new MemoryCacheImageInputStream(
+                new 
ByteArrayInputStream(Base64.getDecoder().decode(data.substring(start))))) {
+            var readers = ImageIO.getImageReaders(stream);
+            if (!readers.hasNext()) {
+                return unavailable(label, "no local decoder for this image");
+            }
+            var reader = readers.next();
+            try {
+                reader.setInput(stream, true, true);
+                int width = reader.getWidth(0);
+                int height = reader.getHeight(0);
+                if (width <= 0 || height <= 0 || (long) width * height > 
40_000_000) {
+                    return unavailable(label, "preview pixel limit");
+                }
+                var parameters = reader.getDefaultReadParam();
+                int sample = Math.max(1, (Math.max(width, height) + 63) / 64);
+                parameters.setSourceSubsampling(sample, sample, 0, 0);
+                BufferedImage image = reader.read(0, parameters);
+                List<List<Integer>> rows = new ArrayList<>();
+                for (int y = 0; y < image.getHeight(); y++) {
+                    List<Integer> row = new ArrayList<>();
+                    for (int x = 0; x < image.getWidth(); x++) {
+                        row.add(onWhite(image.getRGB(x, y)));
+                    }
+                    rows.add(row);
+                }
+                return new SemanticImagePreview(label + " · " + width + " × " 
+ height + " · terminal preview", rows);
+            } finally {
+                reader.dispose();
+            }
+        } catch (Exception failure) {
+            return unavailable(label, "image could not be decoded");
+        }
+    }
+
+    private static SemanticImagePreview unavailable(String label, String 
reason) {
+        return new SemanticImagePreview(label + " · preview unavailable: " + 
reason, List.of());
+    }
+
+    private static int onWhite(int argb) {
+        int alpha = argb >>> 24;
+        int red = ((argb >> 16 & 255) * alpha + 255 * (255 - alpha)) / 255;
+        int green = ((argb >> 8 & 255) * alpha + 255 * (255 - alpha)) / 255;
+        int blue = ((argb & 255) * alpha + 255 * (255 - alpha)) / 255;
+        return red << 16 | green << 8 | blue;
+    }
+
+    /** Plain terminal fallback; the TUI renders the same pixels with 
foreground/background colours. */
+    public List<String> textLines() {
+        List<String> lines = new ArrayList<>();
+        lines.add(description);
+        for (int y = 0; y < pixels.size(); y += 2) {
+            StringBuilder line = new StringBuilder();
+            for (int rgb : pixels.get(y)) {
+                int luminance = ((rgb >> 16 & 255) * 299 + (rgb >> 8 & 255) * 
587 + (rgb & 255) * 114) / 1000;
+                line.append(" .:-=+*#%@".charAt(luminance * 9 / 255));
+            }
+            lines.add(line.toString());
+        }
+        return List.copyOf(lines);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/SemanticMediaInput.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/SemanticMediaInput.java
new file mode 100644
index 000000000000..8d332eb02188
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/SemanticMediaInput.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Base64;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/** Media input shared by the CLI and TUI; independent of expert 
implementations. */
+public final class SemanticMediaInput {
+    private static final String PARTS = "$semanticInput";
+
+    private SemanticMediaInput() {
+    }
+
+    public static boolean isMedia(Object input) {
+        return input instanceof Map<?, ?> map && map.get(PARTS) instanceof 
List<?>;
+    }
+
+    public static List<?> parts(Object input) {
+        return isMedia(input) ? (List<?>) ((Map<?, ?>) input).get(PARTS) : 
List.of();
+    }
+
+    /** Read only an explicitly requested local image. This must run off the 
TUI render thread. */
+    public static Object readImage(String value) {
+        if (!value.startsWith("image:")) {
+            return value;
+        }
+        Path path = Path.of(value.substring(6));
+        String name = path.getFileName().toString().toLowerCase(Locale.ROOT);
+        String type = name.endsWith(".png") ? "image/png"
+                : name.endsWith(".jpg") || name.endsWith(".jpeg") ? 
"image/jpeg"
+                : name.endsWith(".gif") ? "image/gif" : name.endsWith(".webp") 
? "image/webp" : null;
+        if (type == null) {
+            throw new IllegalArgumentException("image: requires a local PNG, 
JPEG, GIF or WebP file");
+        }
+        try {
+            String data = "data:" + type + ";base64," + 
Base64.getEncoder().encodeToString(Files.readAllBytes(path));
+            return Map.of(PARTS, List.of(Map.of("type", "image", "data", 
data)));
+        } catch (IOException e) {
+            throw new IllegalArgumentException("Cannot read the selected image 
file", e);
+        }
+    }
+
+    /** Preserve metadata in terminal/JSON displays without printing image 
payloads. */
+    public static Object withoutImageData(Object value) {
+        if (value instanceof Map<?, ?> map) {
+            if (isMedia(map)) {
+                JsonArray parts = new JsonArray();
+                for (Object item : parts(map)) {
+                    parts.add(item instanceof Map<?, ?> part && 
"image".equals(part.get("type"))
+                            ? imageDescription(part) : withoutImageData(item));
+                }
+                JsonObject result = new JsonObject();
+                result.put(PARTS, parts);
+                return result;
+            }
+            JsonObject result = new JsonObject();
+            map.forEach((key, item) -> result.put(key.toString(), 
withoutImageData(item)));
+            return result;
+        }
+        if (value instanceof List<?> list) {
+            JsonArray result = new JsonArray();
+            list.forEach(item -> result.add(withoutImageData(item)));
+            return result;
+        }
+        if (value instanceof String text && text.startsWith("data:image/")) {
+            return imageDescription(Map.of("type", "image", "data", text));
+        }
+        return value;
+    }
+
+    public static JsonObject imageDescription(Map<?, ?> part) {
+        JsonObject result = new JsonObject();
+        result.put("type", "image");
+        if (part.get("data") instanceof String data) {
+            int start = imageDataOffset(data);
+            if (start >= 0) {
+                result.put("mediaType", data.substring(5, start - 8));
+                result.put("encodedCharacters", data.length() - start);
+            }
+            result.put("capture", "available");
+            result.put("dataOmitted", "display");
+        } else {
+            result.put("capture", "omitted");
+            result.put("dataOmitted", part.getOrDefault("dataOmitted", null));
+            if (part.get("mediaType") instanceof String type && 
type.matches("image/[a-z0-9.+-]{1,100}")) {
+                result.put("mediaType", type);
+            }
+        }
+        return result;
+    }
+
+    /** Offset of the base64 payload, or -1 when the inline image prefix is 
invalid. */
+    static int imageDataOffset(String data) {
+        int marker = data.indexOf(";base64,");
+        return data.startsWith("data:image/") && marker > 5 && marker < 120
+                && data.substring(5, marker).matches("image/[a-z0-9.+-]+") ? 
marker + 8 : -1;
+    }
+
+    public static String typeLabel(Object type) {
+        return switch (type instanceof String text ? text : "") {
+            case "image" -> "Image";
+            case "text-and-images" -> "Text + images";
+            case "text" -> "Text";
+            case "structured" -> "Structured";
+            case "unsupported" -> "Unsupported input";
+            case "unknown" -> "Not determined";
+            default -> "Not recorded";
+        };
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/SemanticResultFormatter.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/SemanticResultFormatter.java
index db7360ce142b..b971e713e1b9 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/SemanticResultFormatter.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/SemanticResultFormatter.java
@@ -90,8 +90,9 @@ public final class SemanticResultFormatter {
                     int lower = (int) Math.floor(position);
                     int upper = (int) Math.ceil(position);
                     lines.add(lower == upper
-                            ? level(levels, lower)
-                            : "Between " + level(levels, lower) + " and " + 
level(levels, upper));
+                            ? level(levels, lower, interpretation)
+                            : "Between " + level(levels, lower, 
interpretation) + " and "
+                              + level(levels, upper, interpretation));
                 } else {
                     lines.add("Score outside the captured scale; numeric value 
preserved");
                 }
@@ -161,7 +162,7 @@ public final class SemanticResultFormatter {
                     }
                     for (int i = 0; i < levels.size(); i++) {
                         if (Integer.toString(i).equals(entry.getKey())) {
-                            modes.add(level(levels, i));
+                            modes.add(level(levels, i, interpretation));
                         }
                     }
                 }
@@ -178,8 +179,10 @@ public final class SemanticResultFormatter {
         return value instanceof Map<?, ?> map ? map : Map.of();
     }
 
-    private static String level(List<?> levels, int index) {
-        return "[" + index + "] " + text(levels.get(index));
+    private static String level(List<?> levels, int index, Map<?, ?> 
interpretation) {
+        String label = text(levels.get(index));
+        Object description = 
map(interpretation.get("scoreDescriptions")).get(levels.get(index));
+        return "[" + index + "] " + label + (description == null ? "" : " — " 
+ text(description));
     }
 
     private static String value(Object value) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java
index c5b2e473af00..5d01d33b5d1f 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java
@@ -58,6 +58,58 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class SemanticCommandTest {
+    @Test
+    void imageFileBodyIsSentAsExplicitMedia() throws Exception {
+        Path image = home.resolve("input.png");
+        Files.write(image, new byte[] { 1, 2, 3 });
+        assertEquals(0, respond(new SemanticEvaluate(main()), request -> {
+            assertEquals(Map.of("$semanticInput", List.of(Map.of("type", 
"image", "data", "data:image/png;base64,AQID"))),
+                    request.get("body"));
+            return jsonUnchecked("{\"status\":\"success\",\"value\":true}");
+        }, "--evaluation=inspect", "--body=image:" + image));
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void imageAuditDisplaysTypeWithoutPrintingPayload(boolean json) throws 
Exception {
+        var response = new JsonObject(
+                Map.of("record", new JsonObject(
+                        Map.of("eventId", "image-event", "inputType", "image",
+                                "input", Map.of("$semanticInput",
+                                        List.of(Map.of("type", "image", 
"data", "data:image/png;base64,AQID")))))));
+        assertEquals(0, respond(new SemanticAudit(main()), request -> response,
+                "--event-id=image-event", "--json=" + json));
+        if (json) {
+            Map<?, ?> input = (Map<?, ?>) 
json(printer.getOutput()).getMap("record").get("input");
+            List<?> parts = (List<?>) input.get("$semanticInput");
+            assertEquals("image/png", ((Map<?, ?>) 
parts.get(0)).get("mediaType"));
+        } else {
+            assertTrue(printer.getOutput().contains("image/png"));
+        }
+        assertFalse(printer.getOutput().contains("base64"));
+        if (!json) {
+            assertTrue(printer.getOutput().contains("Input type: Image"));
+            assertTrue(printer.getOutput().contains("preview unavailable"));
+        }
+    }
+
+    @Test
+    void explicitFileExportPreservesCapturedImageAndNeverOverwrites() throws 
Exception {
+        Path target = home.resolve("audit.json");
+        var response = new JsonObject(
+                Map.of("record", new JsonObject(
+                        Map.of("eventId", "image-event", "inputType", "image",
+                                "input", Map.of("$semanticInput",
+                                        List.of(Map.of("type", "image", 
"data", "data:image/png;base64,AQID")))))));
+        assertEquals(0, respond(new SemanticAudit(main()), request -> response,
+                "--event-id=image-event", "--output=" + target));
+        assertEquals(response, json(Files.readString(target)));
+        assertFalse(printer.getOutput().contains("base64"));
+        assertEquals(1, respond(new SemanticAudit(main()), request -> response,
+                "--event-id=image-event", "--output=" + target));
+        assertTrue(errors.toString().contains("file already exists"));
+        assertEquals(response, json(Files.readString(target)));
+    }
 
     @TempDir
     Path home;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/SemanticMediaInputTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/SemanticMediaInputTest.java
new file mode 100644
index 000000000000..68cbd070f22d
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/SemanticMediaInputTest.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.nio.file.Path;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+import javax.imageio.ImageIO;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class SemanticMediaInputTest {
+    @TempDir
+    Path directory;
+
+    @Test
+    void imageFilesBecomeExplicitMediaAndDisplaysNeverContainBase64() throws 
Exception {
+        var image = new BufferedImage(4, 4, BufferedImage.TYPE_INT_RGB);
+        image.setRGB(0, 0, 0xff0000);
+        Path path = directory.resolve("sample.png");
+        ImageIO.write(image, "png", path.toFile());
+        Object input = SemanticMediaInput.readImage("image:" + path);
+        assertTrue(SemanticMediaInput.isMedia(input));
+        var description = 
SemanticMediaInput.withoutImageData(input).toString();
+        assertTrue(description.contains("image/png"));
+        assertTrue(description.contains("available"));
+        assertFalse(description.contains("base64"));
+        var previews = SemanticImagePreview.prepare(input);
+        assertEquals(1, previews.size());
+        assertEquals(0xff0000, previews.get(0).pixels().get(0).get(0));
+        assertTrue(previews.get(0).description().contains("4 × 4"));
+        assertFalse(previews.get(0).textLines().toString().contains("base64"));
+    }
+
+    @Test
+    void previewIsBoundedAndCorruptOrOmittedImagesAreExplicit() throws 
Exception {
+        var image = new BufferedImage(200, 100, BufferedImage.TYPE_INT_RGB);
+        var buffer = new ByteArrayOutputStream();
+        ImageIO.write(image, "png", buffer);
+        String data = "data:image/png;base64," + 
Base64.getEncoder().encodeToString(buffer.toByteArray());
+        var input = Map.of("$semanticInput", List.of(Map.of("type", "image", 
"data", data),
+                Map.of("type", "image", "data", 
"data:image/png;base64,broken"),
+                Map.of("type", "image", "dataOmitted", "snapshot_limit", 
"mediaType", "image/png")));
+        var previews = SemanticImagePreview.prepare(input);
+        assertTrue(previews.get(0).pixels().get(0).size() <= 64);
+        assertTrue(previews.get(0).pixels().size() <= 64);
+        assertTrue(previews.get(1).pixels().isEmpty());
+        assertTrue(previews.get(1).description().contains("unavailable"));
+        assertTrue(previews.get(2).description().contains("not retained"));
+        assertEquals("Text + images", 
SemanticMediaInput.typeLabel("text-and-images"));
+    }
+
+    @Test
+    void transparentPixelsAreCompositedOnWhite() throws Exception {
+        var image = new BufferedImage(2, 1, BufferedImage.TYPE_INT_ARGB);
+        image.setRGB(0, 0, 0x00ff0000);
+        image.setRGB(1, 0, 0x80000000);
+        var buffer = new ByteArrayOutputStream();
+        ImageIO.write(image, "png", buffer);
+        String data = "data:image/png;base64," + 
Base64.getEncoder().encodeToString(buffer.toByteArray());
+        var input = Map.of("$semanticInput", List.of(Map.of("type", "image", 
"data", data)));
+        var preview = SemanticImagePreview.prepare(input).get(0);
+        assertEquals(List.of(0xffffff, 0x7f7f7f), preview.pixels().get(0));
+    }
+
+    @Test
+    void ordinaryStateAndTextKeepTheirMeaning() {
+        Object input = Map.of("type", "image", "data", "application state");
+        assertEquals(input, SemanticMediaInput.withoutImageData(input));
+        assertEquals("https://example.com/picture.png";, 
SemanticMediaInput.readImage("https://example.com/picture.png";));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/SemanticResultFormatterTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/SemanticResultFormatterTest.java
index 42777ea03fff..aa5ef92e8325 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/SemanticResultFormatterTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/SemanticResultFormatterTest.java
@@ -61,6 +61,16 @@ class SemanticResultFormatterTest {
         assertFalse(String.join(" ", answer.details()).contains("severity"));
     }
 
+    @Test
+    void includesCapturedRubricDescriptionsWithoutChangingTheScore() {
+        var answer = SemanticResultFormatter.audit(Map.of("status", "success", 
"semantics", Map.of("resultType", "SCORE"),
+                "result", Map.of("value", 0.96), "interpretation", Map.of(
+                        "scoreLevels", List.of("routine", "urgent"),
+                        "scoreDescriptions", Map.of("routine", "Within a 
week", "urgent", "Immediate action"))));
+        assertEquals("0.96 [SCORE]", answer.compact());
+        assertTrue(answer.details().contains("Between [0] routine — Within a 
week and [1] urgent — Immediate action"));
+    }
+
     @Test
     void reportsIncompleteCapturedDomainWithoutInventingMissingProbabilities() 
{
         var view = SemanticResultFormatter.audit(Map.of("semantics", 
Map.of("resultType", "CHOICE"),
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticAuditView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticAuditView.java
index c89cb843936b..2120ee94b14b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticAuditView.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticAuditView.java
@@ -59,6 +59,7 @@ import dev.tamboui.widgets.table.Row;
 import dev.tamboui.widgets.table.Table;
 import dev.tamboui.widgets.table.TableState;
 import org.apache.camel.dsl.jbang.core.common.SemanticAuditFilters;
+import org.apache.camel.dsl.jbang.core.common.SemanticMediaInput;
 import org.apache.camel.dsl.jbang.core.common.SemanticResultFormatter;
 import org.apache.camel.util.json.JsonObject;
 
@@ -71,6 +72,7 @@ final class SemanticAuditView {
             = Stream.concat(SemanticAuditFilters.QUERY_FIELDS.stream(),
                     Stream.of("page.status", "page.result", 
"page.definition")).collect(Collectors.toUnmodifiableSet());
     private final MonitorContext context;
+    private final SemanticImageView imageView;
     private final LongSupplier nanoTime;
     private final Supplier<Instant> now;
     private long lastRefresh;
@@ -121,11 +123,13 @@ final class SemanticAuditView {
 
     SemanticAuditView(MonitorContext context, LongSupplier nanoTime, 
Supplier<Instant> now) {
         this.context = context;
+        this.imageView = new SemanticImageView(context.backgroundExecutor);
         this.nanoTime = nanoTime;
         this.now = now;
     }
 
     void reset() {
+        imageView.reset();
         if (pending != null) {
             pending.cancel(true);
         }
@@ -373,6 +377,7 @@ final class SemanticAuditView {
         }
         caseIndex = 0;
         showInput = false;
+        imageView.reset();
         details = null;
         scroll = 0;
         if (pendingDetails != null) {
@@ -875,7 +880,8 @@ final class SemanticAuditView {
             String expert = text(row, "expert") + (wide && row.get("provider") 
!= null ? " / " + text(row, "provider") : "");
             content.add(wide
                     ? Row.from(Cell.from(time(row, "MM-dd HH:mm:ss")), 
Cell.from(executingRoute(row)),
-                            Cell.from(text(row, "definition")), 
Cell.from(expert), result, execution,
+                            Cell.from(text(row, "definition")), 
Cell.from(expert),
+                            
Cell.from(SemanticMediaInput.typeLabel(row.get("inputType"))), result, 
execution,
                             Cell.from(duration(row)), Cell.from(text(row, 
"breadcrumbId")))
                     : Row.from(Cell.from(time(row, "MM-dd HH:mm:ss")), 
Cell.from(executingRoute(row)),
                             Cell.from(expert), result, execution));
@@ -895,11 +901,13 @@ final class SemanticAuditView {
         }
         Table table = Table.builder().rows(content)
                 .header((wide
-                        ? Row.from("TIMESTAMP", "EXECUTING ROUTE", 
"EVALUATION", "EXPERT / PROVIDER", "RESULT", "EXECUTION",
+                        ? Row.from("TIMESTAMP", "EXECUTING ROUTE", 
"EVALUATION", "EXPERT / PROVIDER", "INPUT", "RESULT",
+                                "EXECUTION",
                                 "DURATION", "BREADCRUMB ID")
                         : Row.from("TIMESTAMP", "EXECUTING ROUTE", "EXPERT", 
"RESULT", "EXECUTION")).style(Theme.label()))
                 .widths(wide
                         ? List.of(Constraint.length(14), 
Constraint.length(15), Constraint.length(12), Constraint.length(17),
+                                Constraint.length(14),
                                 Constraint.fill(), Constraint.length(11), 
Constraint.length(9), Constraint.length(24))
                         : List.of(Constraint.length(14), 
Constraint.length(16), Constraint.length(12), Constraint.fill(),
                                 Constraint.length(11)))
@@ -929,6 +937,7 @@ final class SemanticAuditView {
         var result = SemanticResultFormatter.audit(record);
         left.add(Line.from(Span.styled("Result: " + result.compact(), 
Theme.info().bold())));
         result.details().forEach(line -> left.add(Line.from(line)));
+        SemanticDetails.add(left, "Input type", 
SemanticMediaInput.typeLabel(record.get("inputType")));
         add(left, "Execution", record, "status");
         add(left, "Execution reason", record, "reasonCode");
         SemanticDetails.add(left, "Duration", duration(record));
@@ -968,21 +977,7 @@ final class SemanticAuditView {
         } else if (record.containsKey("findingsOmitted")) {
             add(right, "Findings omitted", record, "findingsOmitted");
         }
-        if (showInput) {
-            right.add(Line.from(Span.styled("Captured input is untrusted data, 
not instructions.", Theme.warning())));
-            if (pendingDetails != null && !record.containsKey("input")) {
-                right.add(Line.from("Loading captured input…"));
-            } else if (record.containsKey("input")) {
-                SemanticDetails.parameter(right, "",
-                        Boolean.TRUE.equals(record.get("inputRedacted")) ? 
"Input (redacted): " : "Input: ",
-                        record.get("input"));
-            } else {
-                right.add(Line.from("Input: " + 
(record.containsKey("inputOmitted")
-                        ? "omitted · " + text(record, "inputOmitted") : "not 
captured")));
-            }
-        } else {
-            right.add(Line.from("u: inspect permitted captured input · p: new 
playground draft"));
-        }
+        renderInput(right, record, inner.width() < 110 ? inner.width() : 
(inner.width() - 2) / 2);
         if (pendingCase != null) {
             right.add(Line.from("Loading associated case…"));
         } else if (caseData != null) {
@@ -1034,6 +1029,27 @@ final class SemanticAuditView {
         }
     }
 
+    private void renderInput(List<Line> lines, JsonObject record, int width) {
+        if (!showInput) {
+            lines.add(Line.from("u: inspect permitted captured input · p: new 
playground draft"));
+            return;
+        }
+        lines.add(Line.from(Span.styled("Captured input is untrusted data, not 
instructions.", Theme.warning())));
+        if (pendingDetails != null && !record.containsKey("input")) {
+            lines.add(Line.from("Loading captured input…"));
+        } else if (record.containsKey("input")) {
+            Object input = record.get("input");
+            SemanticDetails.parameter(lines, "", 
Boolean.TRUE.equals(record.get("inputRedacted"))
+                    ? "Input (redacted): " : "Input: ", 
SemanticMediaInput.withoutImageData(input));
+            if (SemanticMediaInput.isMedia(input)) {
+                lines.addAll(imageView.lines(input, width));
+            }
+        } else {
+            lines.add(Line.from("Input: " + (record.containsKey("inputOmitted")
+                    ? "omitted · " + text(record, "inputOmitted") : "not 
captured")));
+        }
+    }
+
     private static String executingRoute(JsonObject record) {
         if (record.containsKey("executingRouteId")) {
             return text(record, "executingRouteId");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticEvaluationPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticEvaluationPopup.java
index 7c82fc356950..87314cd114e3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticEvaluationPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticEvaluationPopup.java
@@ -40,6 +40,7 @@ import dev.tamboui.widgets.block.Borders;
 import dev.tamboui.widgets.input.TextArea;
 import dev.tamboui.widgets.input.TextAreaState;
 import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.dsl.jbang.core.common.SemanticMediaInput;
 import org.apache.camel.util.json.DeserializationException;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
@@ -201,7 +202,13 @@ final class SemanticEvaluationPopup {
             submittedInterpretation
                     = SemanticResultView.interpretation(operation, 
definition.get("parameters"), submittedScoreLevels);
             pending = CompletableFuture
-                    .supplyAsync(() -> ctx.executeIndependentAction(pid, 
request, 60000), ctx.backgroundExecutor)
+                    .supplyAsync(() -> {
+                        JsonObject prepared = new JsonObject(request);
+                        if (prepared.get("body") instanceof String text) {
+                            prepared.put("body", 
SemanticMediaInput.readImage(text));
+                        }
+                        return ctx.executeIndependentAction(pid, prepared, 
60000);
+                    }, ctx.backgroundExecutor)
                     .exceptionally(SemanticResultView::failure);
         } catch (Exception invalid) {
             error = "Input error: " + invalid.getMessage();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticImageView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticImageView.java
new file mode 100644
index 000000000000..c20e385d89ac
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticImageView.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import org.apache.camel.dsl.jbang.core.common.SemanticImagePreview;
+
+/** Decode off the render thread; draw only bounded pixels from the selected 
captured input. */
+final class SemanticImageView {
+    private final Executor executor;
+    private Object selected;
+    private CompletableFuture<List<SemanticImagePreview>> pending;
+
+    SemanticImageView(Executor executor) {
+        this.executor = executor;
+    }
+
+    void reset() {
+        if (pending != null) {
+            pending.cancel(true);
+        }
+        selected = null;
+        pending = null;
+    }
+
+    List<Line> lines(Object input, int width) {
+        if (selected != input) {
+            reset();
+            selected = input;
+            pending = CompletableFuture.supplyAsync(() -> 
SemanticImagePreview.prepare(input), executor)
+                    .exceptionally(failure -> List.of(new 
SemanticImagePreview("Image preview unavailable", List.of())));
+        }
+        if (pending == null || !pending.isDone()) {
+            return List.of(Line.from("Preparing image preview…"));
+        }
+        List<Line> result = new ArrayList<>();
+        for (var preview : pending.getNow(List.of())) {
+            result.add(Line.from(Span.styled(preview.description(), 
Theme.label())));
+            result.addAll(pixels(preview, width));
+        }
+        return result;
+    }
+
+    private static List<Line> pixels(SemanticImagePreview preview, int 
availableWidth) {
+        List<List<Integer>> pixels = preview.pixels();
+        if (pixels.isEmpty() || availableWidth <= 0) {
+            return List.of();
+        }
+        int sourceWidth = pixels.get(0).size();
+        int width = Math.min(sourceWidth, availableWidth);
+        int height = Math.max(1, pixels.size() * width / sourceWidth);
+        List<Line> lines = new ArrayList<>();
+        for (int y = 0; y < height; y += 2) {
+            List<Span> spans = new ArrayList<>();
+            for (int x = 0; x < width; x++) {
+                int sourceX = x * sourceWidth / width;
+                int top = pixels.get(y * pixels.size() / height).get(sourceX);
+                int bottom = pixels.get(Math.min(y + 1, height - 1) * 
pixels.size() / height).get(sourceX);
+                spans.add(Span.styled("▀", 
Style.EMPTY.fg(color(top)).bg(color(bottom))));
+            }
+            lines.add(Line.from(spans));
+        }
+        return lines;
+    }
+
+    private static Color color(int rgb) {
+        return Color.rgb(rgb >> 16 & 255, rgb >> 8 & 255, rgb & 255);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticPlayground.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticPlayground.java
index 0636eba05e9c..54c760bd48dd 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticPlayground.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticPlayground.java
@@ -36,6 +36,7 @@ import dev.tamboui.widgets.block.BorderType;
 import dev.tamboui.widgets.block.Borders;
 import dev.tamboui.widgets.input.TextArea;
 import dev.tamboui.widgets.input.TextAreaState;
+import org.apache.camel.dsl.jbang.core.common.SemanticMediaInput;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
 
@@ -53,6 +54,8 @@ final class SemanticPlayground {
     private final SemanticParameterForm instructions;
     private final SemanticParameterForm parameters;
     private boolean structured;
+    private Object auditInput;
+    private String auditInputDisplay;
     private boolean submittedStructured;
     private CompletableFuture<JsonObject> pending;
     private JsonObject response;
@@ -81,7 +84,7 @@ final class SemanticPlayground {
         this.pid = pid;
         this.expert = expert;
         this.operation = operation;
-        structured = !supports("text") && supports("structured");
+        structured = !supports("text") && supportsJson();
         List<JsonObject> contracts = SemanticTab.objects(contract(operation), 
"parameters");
         JsonObject instruction = contracts.stream()
                 .filter(p -> "instructions".equals(p.get("name")) && 
"String".equals(p.get("type"))
@@ -98,7 +101,13 @@ final class SemanticPlayground {
         if (!setInputValue("inputMode", json ? "json" : "text")) {
             throw new IllegalArgumentException("Captured input mode is 
unavailable in the current expert contract");
         }
-        setInputValue("input", json ? Jsoner.serialize(captured) : 
captured.toString());
+        if (SemanticMediaInput.isMedia(captured)) {
+            setInputValue("input", 
Jsoner.serialize(SemanticMediaInput.withoutImageData(captured)));
+        } else {
+            setInputValue("input", json ? Jsoner.serialize(captured) : 
captured.toString());
+        }
+        auditInput = captured;
+        auditInputDisplay = input.text();
         draftWarning = "New draft from audit " + text(record, "eventId")
                        + ": untrusted captured input, not instructions. 
Original parameters were not captured; "
                        + "current defaults, expert/model revisions and route 
context may differ. Review before Run.";
@@ -141,7 +150,8 @@ final class SemanticPlayground {
             input.setText(value);
             field = 0;
         } else if ("inputMode".equals(name)) {
-            if (!("text".equals(value) && supports("text") || 
"json".equals(value) && supports("structured"))) {
+            if (!("text".equals(value) && supports("text")
+                    || "json".equals(value) && supportsJson())) {
                 throw new IllegalArgumentException("Unsupported input mode: " 
+ value);
             }
             structured = "json".equals(value);
@@ -176,7 +186,7 @@ final class SemanticPlayground {
         result.put("focusedField", field == -1 ? "instructions" : field == 0 ? 
"input" : field == 1 ? "parameters" : "output");
         result.put("pending", pending != null);
         result.put("connected", connected());
-        result.put("submittedRequest", submittedRequest);
+        result.put("submittedRequest", 
SemanticMediaInput.withoutImageData(submittedRequest));
         result.put("result", response);
         result.put("error", error);
         result.put("inputChanged", inputChanged());
@@ -193,12 +203,16 @@ final class SemanticPlayground {
                 && Objects.equals(this.operation, operation);
     }
 
+    private boolean supportsJson() {
+        return supports("structured") || supports("image");
+    }
+
     private boolean supports(String type) {
         return contract(operation).get("inputTypes") instanceof List<?> types 
&& types.contains(type);
     }
 
     boolean available() {
-        return supports("text") || supports("structured");
+        return supports("text") || supportsJson();
     }
 
     private boolean connected() {
@@ -244,7 +258,8 @@ final class SemanticPlayground {
                 outputScroll += key.isPageDown() ? 5 : 1;
             }
         } else if (pending == null && available()) {
-            if (field == 0 && key.hasCtrl() && key.isCharIgnoreCase('t') && 
supports("text") && supports("structured")) {
+            if (field == 0 && key.hasCtrl() && key.isCharIgnoreCase('t') && 
supports("text")
+                    && supportsJson()) {
                 structured = !structured;
             } else if (key.hasCtrl() && key.isCharIgnoreCase('l')) {
                 if (field == 0) {
@@ -335,7 +350,12 @@ final class SemanticPlayground {
                 throw invalid;
             }
             Object value = input.text();
-            if (structured) {
+            boolean capturedUnchanged = auditInput != null && 
input.text().equals(auditInputDisplay);
+            // Captured text is evidence, never an implicit instruction to 
read a local image file.
+            boolean readLocalImage = supports("image") && !capturedUnchanged;
+            if (capturedUnchanged && structured && !(auditInput instanceof 
String)) {
+                value = auditInput;
+            } else if (structured && !(readLocalImage && 
input.text().startsWith("image:"))) {
                 value = Jsoner.deserialize(input.text());
                 if (!(value instanceof JsonObject || value instanceof 
List<?>)) {
                     throw new IllegalArgumentException("Structured input must 
be a JSON object or array.");
@@ -355,7 +375,13 @@ final class SemanticPlayground {
             submittedInstructions = instructions.revision();
             submittedParameters = parameters.revision();
             pending = CompletableFuture
-                    .supplyAsync(() -> ctx.executeIndependentAction(pid, 
request, 60000), ctx.backgroundExecutor)
+                    .supplyAsync(() -> {
+                        JsonObject prepared = new JsonObject(request);
+                        if (readLocalImage && prepared.get("input") instanceof 
String text) {
+                            prepared.put("input", 
SemanticMediaInput.readImage(text));
+                        }
+                        return ctx.executeIndependentAction(pid, prepared, 
60000);
+                    }, ctx.backgroundExecutor)
                     .exceptionally(SemanticResultView::failure);
         } catch (Exception invalid) {
             error = "Input error: " + invalid.getMessage();
@@ -457,7 +483,8 @@ final class SemanticPlayground {
         }
         String label = instructions.isEmpty() ? "Input" : structured ? "State 
to assess" : "Text to assess";
         inputArea = renderEditor(frame, stateBox, input, label + " · " + 
(structured ? "JSON" : "text"),
-                focused && field == 0, structured ? "Paste a JSON object or 
array" : "Type or paste the content to assess");
+                focused && field == 0, supports("image") ? "Text, image:/path, 
or a JSON media envelope"
+                        : structured ? "Paste a JSON object or array" : "Type 
or paste the content to assess");
         int outputY = inputBox.bottom();
         if (sideBySide) {
             Rect paramsBox = new Rect(inputBox.right() + 1, y, inner.right() - 
inputBox.right() - 2, editorHeight);
@@ -534,7 +561,7 @@ final class SemanticPlayground {
         }
         hint(spans, "Ctrl+e", expanded ? "restore" : "expand");
         if (field == 0) {
-            if (supports("text") && supports("structured")) {
+            if (supports("text") && supportsJson()) {
                 hint(spans, "Ctrl+t", "text/JSON");
             }
             hintLast(spans, "Enter", "new line");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticResultView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticResultView.java
index e0cd6ccd00c5..3398978bc554 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticResultView.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticResultView.java
@@ -97,6 +97,10 @@ final class SemanticResultView {
             }
         }
         if (parameters instanceof Map<?, ?> values) {
+            Object descriptions = contract.get("scoreDescriptionsParameter");
+            if (descriptions instanceof String parameter && 
values.get(parameter) instanceof Map<?, ?> labels) {
+                result.put("scoreDescriptions", Map.copyOf(labels));
+            }
             for (String kind : List.of("choice", "boolean")) {
                 Object name = contract.get(kind + "LabelsParameter");
                 if (name instanceof String parameter && values.get(parameter) 
instanceof Map<?, ?> labels) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/semantic.md
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/semantic.md
index da6ee25580d6..b653df459c1a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/semantic.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/semantic.md
@@ -98,6 +98,11 @@ JSON), `sample.body` (text), or `sample.headers` / 
`sample.variables` (JSON obje
 Reading or editing never runs the expert; Ctrl+r explicitly evaluates the 
draft.
 
 
+Image-capable experts accept `image:/path/picture.png` or a JSON 
`$semanticInput` envelope.
+The local file is read only on Run. An audit image draft keeps its captured 
payload out of the
+editor and submits it only on an explicit Run; omitted image data cannot be 
reconstructed.
+Unchanged text loaded from audit remains literal text, including an `image:` 
prefix.
+
 ## Audit history
 
 Choose **Audit** to inspect retained evaluations and request outcomes. Result 
and Execution are separate.
@@ -126,6 +131,11 @@ reported probability diagnostics. Historical 
interpretation is captured once; it
   were not captured, and current defaults/model revisions/route context may 
differ. Review the warning and parameters before Run.
 
 The inspector retains full exchange and breadcrumb IDs; tables prioritize 
executing route, evaluation, expert, Result and Execution.
+The Input column and inspector identify images even with payload capture 
disabled. `u` shows
+inline colour previews when captured image data and a local decoder are 
available. Oversized,
+missing or undecodable images show a reason. Preview generation runs in the 
background, never
+fetches a URL, and does not print base64. Stock JDKs may lack a WebP decoder.
+
 Health distinguishes capture settings, reader scope, retention, drops, 
evictions and delivery/query failures.
 An active OpenTelemetry tracer does not prove an external collector received a 
trace.
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticImageViewTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticImageViewTest.java
new file mode 100644
index 000000000000..a293f8df8b44
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticImageViewTest.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.imageio.ImageIO;
+
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+class SemanticImageViewTest {
+    @Test
+    void imageDecodingIsDeferredAndRenderingUsesBoundedColoredPixels() throws 
Exception {
+        var image = new BufferedImage(4, 4, BufferedImage.TYPE_INT_RGB);
+        image.setRGB(0, 0, 0xff0000);
+        var bytes = new ByteArrayOutputStream();
+        ImageIO.write(image, "png", bytes);
+        var input = Map.of("$semanticInput", List.of(Map.of("type", "image", 
"data",
+                "data:image/png;base64," + 
Base64.getEncoder().encodeToString(bytes.toByteArray()))));
+        List<Runnable> work = new ArrayList<>();
+        var view = new SemanticImageView(work::add);
+        assertThat(view.lines(input, 2)).hasSize(1);
+        assertThat(work).hasSize(1);
+        work.get(0).run();
+        var lines = view.lines(input, 2);
+        assertThat(lines).hasSize(2);
+        assertThat(lines.get(1).spans()).hasSize(2);
+        assertThat(lines.get(1).spans().get(0).content()).isEqualTo("▀");
+        assertThat(work).hasSize(1);
+        view.reset();
+        view.lines(input, 2);
+        assertThat(work).hasSize(2);
+    }
+
+    @Test
+    void capturedImageDraftKeepsPayloadOutOfTheEditorAndSendsItOnlyOnRun() {
+        var runtime = new MonitorContext(new AtomicReference<>(List.of()), new 
AtomicReference<>(List.of())) {
+            final List<JsonObject> calls = new CopyOnWriteArrayList<>();
+
+            @Override
+            JsonObject executeIndependentAction(String pid, JsonObject 
request, long timeout) {
+                calls.add(request);
+                return new JsonObject(Map.of("status", "success", "value", 
true));
+            }
+        };
+        try {
+            var integration = new IntegrationInfo();
+            integration.pid = "image";
+            runtime.data.set(List.of(integration));
+            runtime.selectedPid = "image";
+            var operation = new JsonObject(
+                    Map.of("name", "boolean", "contract", new JsonObject(
+                            Map.of("inputTypes", List.of("text", "image"), 
"parameters", List.of()))));
+            var playground = new SemanticPlayground(runtime, "image", 
"expert", operation);
+            var input = Map.of("$semanticInput", List.of(Map.of("type", 
"image", "data", "data:image/png;base64,AQID")));
+            playground.loadAuditInput(new JsonObject(Map.of("eventId", 
"record", "input", input)));
+            
assertThat(playground.snapshot().toJson()).doesNotContain("base64", "AQID");
+            assertThat(runtime.calls).isEmpty();
+            playground.handleKeyEvent(KeyEvent.ofChar('r', KeyModifiers.CTRL));
+            await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> 
assertThat(runtime.calls).hasSize(1));
+            assertThat(runtime.calls.get(0).get("input")).isEqualTo(input);
+            
assertThat(playground.snapshot().toJson()).doesNotContain("base64", "AQID");
+        } finally {
+            runtime.backgroundExecutor.shutdownNow();
+        }
+    }
+
+    @Test
+    void capturedTextIsNeverInterpretedAsALocalImagePath(@TempDir Path 
directory) throws Exception {
+        Path file = directory.resolve("captured.png");
+        Files.write(file, new byte[] { 1, 2, 3 });
+        var runtime = new MonitorContext(new AtomicReference<>(List.of()), new 
AtomicReference<>(List.of())) {
+            final List<JsonObject> calls = new CopyOnWriteArrayList<>();
+
+            @Override
+            JsonObject executeIndependentAction(String pid, JsonObject 
request, long timeout) {
+                calls.add(request);
+                return new JsonObject(Map.of("status", "success", "value", 
true));
+            }
+        };
+        try {
+            var integration = new IntegrationInfo();
+            integration.pid = "image";
+            runtime.data.set(List.of(integration));
+            runtime.selectedPid = "image";
+            var operation = new JsonObject(
+                    Map.of("name", "boolean", "contract", new JsonObject(
+                            Map.of("inputTypes", List.of("text", "image"), 
"parameters", List.of()))));
+            var playground = new SemanticPlayground(runtime, "image", 
"expert", operation);
+            String input = "image:" + file;
+            playground.loadAuditInput(new JsonObject(Map.of("eventId", 
"record", "input", input)));
+            assertThat(runtime.calls).isEmpty();
+            playground.handleKeyEvent(KeyEvent.ofChar('r', KeyModifiers.CTRL));
+            await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> 
assertThat(runtime.calls).hasSize(1));
+            assertThat(runtime.calls.get(0).get("input")).isEqualTo(input);
+            var typed = new SemanticPlayground(runtime, "image", "expert", 
operation);
+            assertThat(typed.setInputValue("input", input)).isTrue();
+            typed.handleKeyEvent(KeyEvent.ofChar('r', KeyModifiers.CTRL));
+            await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> 
assertThat(runtime.calls).hasSize(2));
+            assertThat(runtime.calls.get(1).get("input")).isEqualTo(
+                    Map.of("$semanticInput", List.of(Map.of("type", "image", 
"data", "data:image/png;base64,AQID"))));
+        } finally {
+            runtime.backgroundExecutor.shutdownNow();
+        }
+    }
+}

Reply via email to