This is an automated email from the ASF dual-hosted git repository.
mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 2220b36180 Issue 8350 (#8351)
2220b36180 is described below
commit 2220b36180a8c33e12c7dbdcf21d4842c051b39b
Author: Matt Casters <[email protected]>
AuthorDate: Sun Sep 13 19:12:38 2026 +0200
Issue 8350 (#8351)
* issue #8350 - Fix for the AI advisor pane on Hop Web
* issue #8350 - Fix for the AI advisor pane on Hop Web
---
.../ai/ui/AiAdvisorDesktopTranscriptStyler.java | 71 +++++++++++++
.../org/apache/hop/ai/ui/AiAdvisorSessionPane.java | 5 +-
.../apache/hop/ai/ui/AiAdvisorTranscriptPanel.java | 39 ++-----
.../hop/ai/ui/AiAdvisorWebCompatibilityTest.java | 113 +++++++++++++++++++++
4 files changed, 199 insertions(+), 29 deletions(-)
diff --git
a/plugins/tech/ai/src/main/java/org/apache/hop/ai/ui/AiAdvisorDesktopTranscriptStyler.java
b/plugins/tech/ai/src/main/java/org/apache/hop/ai/ui/AiAdvisorDesktopTranscriptStyler.java
new file mode 100644
index 0000000000..adff88cb1a
--- /dev/null
+++
b/plugins/tech/ai/src/main/java/org/apache/hop/ai/ui/AiAdvisorDesktopTranscriptStyler.java
@@ -0,0 +1,71 @@
+/*
+ * 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.hop.ai.ui;
+
+import org.apache.hop.ai.engine.AiAdvisorMarkdown;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StyleRange;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * Desktop-only markdown transcript renderer using {@link StyledText} and
{@link StyleRange}.
+ * Isolated into a separate class so {@link AiAdvisorTranscriptPanel} can be
loaded in Hop Web
+ * (RAP/RWT) without triggering a {@link NoClassDefFoundError} on desktop-only
SWT classes.
+ */
+class AiAdvisorDesktopTranscriptStyler {
+
+ private AiAdvisorDesktopTranscriptStyler() {}
+
+ static Control createAssistantBody(
+ Composite block, GridData gd, AiAdvisorTranscriptPanel.Role role, String
text) {
+ StyledText body = new StyledText(block, SWT.MULTI | SWT.WRAP |
SWT.READ_ONLY);
+ AiAdvisorMarkdown.Document document = AiAdvisorMarkdown.render(text);
+ body.setText(document.text());
+ body.setLayoutData(gd);
+ AiAdvisorTranscriptPanel.applyRoleLook(body, role);
+ applyMarkdownStyles(body, document);
+ return body;
+ }
+
+ private static void applyMarkdownStyles(StyledText widget,
AiAdvisorMarkdown.Document document) {
+ GuiResource gui = GuiResource.getInstance();
+ for (AiAdvisorMarkdown.Span span : document.spans()) {
+ StyleRange range = new StyleRange();
+ range.start = span.start();
+ range.length = span.length();
+ switch (span.kind()) {
+ case HEADING -> range.font = gui.getFontMediumBold();
+ case BOLD -> range.fontStyle = SWT.BOLD;
+ case EMPHASIS -> range.fontStyle = SWT.ITALIC;
+ case CODE -> range.font = gui.getFontFixed();
+ }
+ widget.setStyleRange(range);
+ }
+ }
+
+ static int lineHeight(Control body) {
+ if (body instanceof StyledText styled) {
+ return styled.getLineHeight();
+ }
+ return 16;
+ }
+}
diff --git
a/plugins/tech/ai/src/main/java/org/apache/hop/ai/ui/AiAdvisorSessionPane.java
b/plugins/tech/ai/src/main/java/org/apache/hop/ai/ui/AiAdvisorSessionPane.java
index 920da97bda..e9bef66663 100644
---
a/plugins/tech/ai/src/main/java/org/apache/hop/ai/ui/AiAdvisorSessionPane.java
+++
b/plugins/tech/ai/src/main/java/org/apache/hop/ai/ui/AiAdvisorSessionPane.java
@@ -64,6 +64,7 @@ import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
import org.apache.hop.ui.hopgui.file.shared.HopGuiAbstractGraph;
import org.apache.hop.ui.hopgui.perspective.TabItemHandler;
import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
+import org.apache.hop.ui.util.EnvironmentUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.layout.FormAttachment;
@@ -134,7 +135,9 @@ public class AiAdvisorSessionPane extends Composite {
wPrompt = new Text(this, SWT.MULTI | SWT.WRAP | SWT.BORDER | SWT.V_SCROLL);
applyPromptFieldLook(wPrompt);
wPrompt.setMessage(BaseMessages.getString(PKG,
"AiAdvisor.Prompt.Message"));
- wPrompt.addPaintListener(e -> paintPromptHint(wPrompt, e));
+ if (!EnvironmentUtils.getInstance().isWeb()) {
+ wPrompt.addPaintListener(e -> paintPromptHint(wPrompt, e));
+ }
wPrompt.setLayoutData(
new FormDataBuilder()
.left(wQuestion, margin)
diff --git
a/plugins/tech/ai/src/main/java/org/apache/hop/ai/ui/AiAdvisorTranscriptPanel.java
b/plugins/tech/ai/src/main/java/org/apache/hop/ai/ui/AiAdvisorTranscriptPanel.java
index 3403be2ad7..e63050f419 100644
---
a/plugins/tech/ai/src/main/java/org/apache/hop/ai/ui/AiAdvisorTranscriptPanel.java
+++
b/plugins/tech/ai/src/main/java/org/apache/hop/ai/ui/AiAdvisorTranscriptPanel.java
@@ -27,10 +27,9 @@ import org.apache.hop.core.util.Utils;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.util.EnvironmentUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
-import org.eclipse.swt.custom.StyleRange;
-import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Point;
@@ -318,39 +317,23 @@ public class AiAdvisorTranscriptPanel extends Composite {
private void appendBody(Composite block, Role role, String text) {
GridData gd = new GridData(SWT.FILL, SWT.FILL, true, false);
gd.widthHint = 1;
- if (role == Role.USER) {
+ if (role == Role.USER || EnvironmentUtils.getInstance().isWeb()) {
Text body = new Text(block, SWT.MULTI | SWT.WRAP | SWT.READ_ONLY);
- body.setText(text != null ? text : "");
+ if (role == Role.USER) {
+ body.setText(text != null ? text : "");
+ } else {
+ AiAdvisorMarkdown.Document document = AiAdvisorMarkdown.render(text);
+ body.setText(document.text());
+ }
body.setLayoutData(gd);
applyRoleLook(body, role);
bodies.add(body);
return;
}
- StyledText body = new StyledText(block, SWT.MULTI | SWT.WRAP |
SWT.READ_ONLY);
- AiAdvisorMarkdown.Document document = AiAdvisorMarkdown.render(text);
- body.setText(document.text());
- body.setLayoutData(gd);
- applyRoleLook(body, role);
- applyMarkdownStyles(body, document);
+ Control body = AiAdvisorDesktopTranscriptStyler.createAssistantBody(block,
gd, role, text);
bodies.add(body);
}
- private void applyMarkdownStyles(StyledText widget,
AiAdvisorMarkdown.Document document) {
- GuiResource gui = GuiResource.getInstance();
- for (AiAdvisorMarkdown.Span span : document.spans()) {
- StyleRange range = new StyleRange();
- range.start = span.start();
- range.length = span.length();
- switch (span.kind()) {
- case HEADING -> range.font = gui.getFontMediumBold();
- case BOLD -> range.fontStyle = SWT.BOLD;
- case EMPHASIS -> range.fontStyle = SWT.ITALIC;
- case CODE -> range.font = gui.getFontFixed();
- }
- widget.setStyleRange(range);
- }
- }
-
/**
* Subtle role colors so questions and answers are distinct in both light
and dark mode. Values
* are {background R,G,B, foreground R,G,B}.
@@ -455,8 +438,8 @@ public class AiAdvisorTranscriptPanel extends Composite {
if (body instanceof Text text) {
return text.getLineHeight();
}
- if (body instanceof StyledText styled) {
- return styled.getLineHeight();
+ if (!EnvironmentUtils.getInstance().isWeb()) {
+ return AiAdvisorDesktopTranscriptStyler.lineHeight(body);
}
return 16;
}
diff --git
a/plugins/tech/ai/src/test/java/org/apache/hop/ai/ui/AiAdvisorWebCompatibilityTest.java
b/plugins/tech/ai/src/test/java/org/apache/hop/ai/ui/AiAdvisorWebCompatibilityTest.java
new file mode 100644
index 0000000000..37eaf6b389
--- /dev/null
+++
b/plugins/tech/ai/src/test/java/org/apache/hop/ai/ui/AiAdvisorWebCompatibilityTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.hop.ai.ui;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Hop Web runs on RAP/RWT, which ships an SWT subset. Classes loaded in Hop
Web must not mention
+ * desktop-only SWT types in field or method signatures, as reflection or
class linking will fail
+ * with {@link NoClassDefFoundError}.
+ */
+class AiAdvisorWebCompatibilityTest {
+
+ private static final Set<String> DESKTOP_ONLY_SWT_TYPES =
+ new HashSet<>(
+ Arrays.asList(
+ "org.eclipse.swt.custom.BidiSegmentListener",
+ "org.eclipse.swt.custom.Bullet",
+ "org.eclipse.swt.custom.CTabFolderRenderer",
+ "org.eclipse.swt.custom.CaretListener",
+ "org.eclipse.swt.custom.ExtendedModifyListener",
+ "org.eclipse.swt.custom.LineBackgroundListener",
+ "org.eclipse.swt.custom.LineStyleEvent",
+ "org.eclipse.swt.custom.LineStyleListener",
+ "org.eclipse.swt.custom.PaintObjectListener",
+ "org.eclipse.swt.custom.PopupList",
+ "org.eclipse.swt.custom.ST",
+ "org.eclipse.swt.custom.StyleRange",
+ "org.eclipse.swt.custom.StyledText",
+ "org.eclipse.swt.custom.StyledTextContent",
+ "org.eclipse.swt.custom.TableCursor",
+ "org.eclipse.swt.custom.TreeCursor",
+ "org.eclipse.swt.custom.VerifyKeyListener",
+ "org.eclipse.swt.graphics.GlyphMetrics",
+ "org.eclipse.swt.graphics.Pattern",
+ "org.eclipse.swt.graphics.Region",
+ "org.eclipse.swt.graphics.TextLayout",
+ "org.eclipse.swt.graphics.TextStyle",
+ "org.eclipse.swt.widgets.Caret",
+ "org.eclipse.swt.widgets.Tracker"));
+
+ private static final List<Class<?>> WEB_ELIGIBLE_UI_CLASSES =
+ List.of(
+ AiAdvisorPerspective.class,
+ AiAdvisorWorkbench.class,
+ AiAdvisorSessionPane.class,
+ AiAdvisorTranscriptPanel.class,
+ AiAdvisorViews.class,
+ AiAdvisorProposalReviewDialog.class,
+ AiAdvisorMetadataSelectionDialog.class,
+ AiAdvisorDialog.class,
+ PipelineAiGuiPlugin.class,
+ WorkflowAiGuiPlugin.class);
+
+ @Test
+ void uiClassesAvoidDesktopOnlySwtTypesInSignatures() {
+ List<String> violations = new ArrayList<>();
+
+ for (Class<?> clazz : WEB_ELIGIBLE_UI_CLASSES) {
+ for (Field field : clazz.getDeclaredFields()) {
+ if (DESKTOP_ONLY_SWT_TYPES.contains(field.getType().getName())) {
+ violations.add(
+ clazz.getName()
+ + ": field "
+ + field.getName()
+ + " of type "
+ + field.getType().getName());
+ }
+ }
+ for (Method method : clazz.getDeclaredMethods()) {
+ List<Class<?>> types = new ArrayList<>();
+ types.add(method.getReturnType());
+ types.addAll(Arrays.asList(method.getParameterTypes()));
+ for (Class<?> type : types) {
+ if (DESKTOP_ONLY_SWT_TYPES.contains(type.getName())) {
+ violations.add(
+ clazz.getName() + ": method " + method.getName() + " uses " +
type.getName());
+ }
+ }
+ }
+ }
+
+ assertTrue(
+ violations.isEmpty(),
+ "UI classes eligible for Hop Web must not name desktop-only SWT types
in field or method"
+ + " signatures: "
+ + violations);
+ }
+}