diff --git a/diylc/diylc-core/pom.xml b/diylc/diylc-core/pom.xml
index 4271f479..de7951dc 100644
--- a/diylc/diylc-core/pom.xml
+++ b/diylc/diylc-core/pom.xml
@@ -7,7 +7,7 @@
org.diylc
diylc-parent
- 5.2.0
+ 5.3.0
diylc-core
diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/EventType.java b/diylc/diylc-core/src/main/java/org/diylc/common/EventType.java
index 87a57fcb..0600b408 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/common/EventType.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/common/EventType.java
@@ -45,6 +45,10 @@ public enum EventType {
* has been either reloaded or loaded with undo/redo operations.
*/
PROJECT_LOADED,
+ /**
+ * Called when the current project is saved to a file. The first parameter is the current file name.
+ */
+ PROJECT_SAVED,
/**
* Called when either current file name or modified flag are changed. The first parameter is the
* current file name, the second one is a boolean modified flag.
diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/IServiceManager.java b/diylc/diylc-core/src/main/java/org/diylc/common/IServiceManager.java
index a4a93d60..156face9 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/common/IServiceManager.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/common/IServiceManager.java
@@ -3,6 +3,7 @@ package org.diylc.common;
import org.diylc.appframework.simplemq.MessageDispatcher;
import org.diylc.plugins.chatbot.service.ChatbotService;
import org.diylc.plugins.cloud.service.CloudService;
+import org.diylc.presenter.DrawingManager;
public interface IServiceManager {
@@ -11,4 +12,6 @@ public interface IServiceManager {
ChatbotService getChatbotService();
MessageDispatcher getMessageDispatcher();
+
+ DrawingManager getDrawingManager();
}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/netlist/IGuitarDiagramAnalyzer.java b/diylc/diylc-core/src/main/java/org/diylc/netlist/IGuitarDiagramAnalyzer.java
new file mode 100644
index 00000000..6d1f8971
--- /dev/null
+++ b/diylc/diylc-core/src/main/java/org/diylc/netlist/IGuitarDiagramAnalyzer.java
@@ -0,0 +1,8 @@
+package org.diylc.netlist;
+
+import java.util.List;
+
+public interface IGuitarDiagramAnalyzer {
+
+ List collectNotes(Netlist netlist);
+}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiComponent.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiComponent.java
new file mode 100644
index 00000000..905e34ff
--- /dev/null
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiComponent.java
@@ -0,0 +1,6 @@
+package org.diylc.plugins.chatbot.model;
+
+import java.util.List;
+
+public record AiComponent(String id, String type, String value, AiPoint fromPos, AiPoint toPos, AiPoint pos, List terminals) {
+}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiPoint.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiPoint.java
new file mode 100644
index 00000000..89f6c619
--- /dev/null
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiPoint.java
@@ -0,0 +1,4 @@
+package org.diylc.plugins.chatbot.model;
+
+public record AiPoint(int x, int y) {
+}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiProject.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiProject.java
new file mode 100644
index 00000000..27343255
--- /dev/null
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiProject.java
@@ -0,0 +1,12 @@
+package org.diylc.plugins.chatbot.model;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public record AiProject(Map metadata,
+ List tags,
+ List components,
+ Map> nets,
+ List switches) {
+}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiSwitch.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiSwitch.java
new file mode 100644
index 00000000..3c405f83
--- /dev/null
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiSwitch.java
@@ -0,0 +1,6 @@
+package org.diylc.plugins.chatbot.model;
+
+import java.util.List;
+
+public record AiSwitch(String componentId, List positions) {
+}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiSwitchPosition.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiSwitchPosition.java
new file mode 100644
index 00000000..8eb0027b
--- /dev/null
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiSwitchPosition.java
@@ -0,0 +1,7 @@
+package org.diylc.plugins.chatbot.model;
+
+import java.util.List;
+import java.util.Set;
+
+public record AiSwitchPosition(String name, List> connectedTerminalGroups) {
+}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiTerminal.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiTerminal.java
new file mode 100644
index 00000000..117e9a7a
--- /dev/null
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/model/AiTerminal.java
@@ -0,0 +1,4 @@
+package org.diylc.plugins.chatbot.model;
+
+public record AiTerminal(int id, String label, AiPoint pos) {
+}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/AiProjectBuilder.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/AiProjectBuilder.java
new file mode 100644
index 00000000..e7388a5c
--- /dev/null
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/AiProjectBuilder.java
@@ -0,0 +1,165 @@
+package org.diylc.plugins.chatbot.service;
+
+import java.awt.*;
+import java.awt.geom.Point2D;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import org.apache.log4j.Logger;
+import org.diylc.common.ComponentType;
+import org.diylc.common.PropertyWrapper;
+import org.diylc.core.IDIYComponent;
+import org.diylc.core.ISwitch;
+import org.diylc.core.Project;
+import org.diylc.plugins.chatbot.model.*;
+import org.diylc.presenter.ComponentProcessor;
+import org.diylc.presenter.ContinuityArea;
+
+public class AiProjectBuilder {
+
+ public static AiProject build(Project project, List continuityAreas) {
+
+ Map metadata = getMetadata(project);
+ List tags = getTags(project);
+
+ List components = project.getComponents().stream().map(AiProjectBuilder::mapComponent).toList();
+
+ List> sets =
+ NetlistBuilder.buildNets(project.getComponents(), continuityAreas);
+
+ AtomicInteger counter = new AtomicInteger();
+ Map> nets =
+ sets.stream().map(s -> s.stream()
+ .map(AiProjectBuilder::mapTerminal)
+ .collect(Collectors.toSet()))
+ .collect(Collectors.toMap(x -> "N" + counter.incrementAndGet(), x -> x));
+
+ List switches =
+ project.getComponents().stream()
+ .filter(x -> x instanceof ISwitch)
+ .map(x -> (ISwitch) x)
+ .map(AiProjectBuilder::mapSwitch).toList();
+
+ return new AiProject(metadata, tags, components, nets, switches);
+ }
+
+ private static Map getMetadata(Project project) {
+ Map metadata = new HashMap<>();
+ if (project.getTitle() != null) metadata.put("title", project.getTitle());
+ if (project.getAuthor() != null) metadata.put("author", project.getAuthor());
+ if (project.getDescription() != null) metadata.put("description", project.getDescription());
+ metadata.put("gridSpacing", project.getGridSpacing().toString());
+ metadata.put("width", project.getWidth().toString());
+ metadata.put("height", project.getHeight().toString());
+ return metadata;
+ }
+
+ private static List getTags(Project project) {
+
+ List tags = new ArrayList<>();
+ if (project.getComponents().stream().anyMatch(x -> x.getClass().getCanonicalName().toLowerCase().contains("guitar"))) {
+ tags.add("guitar");
+ }
+ if (project.getComponents().stream().anyMatch(x -> x.getClass().getName().toLowerCase().contains("symbol"))) {
+ tags.add("schematic");
+ }
+ if (project.getComponents().stream().anyMatch(x -> x.getClass().getName().toLowerCase().contains("board")) &&
+ project.getComponents().stream().anyMatch(x -> x.getClass().getName().toLowerCase().contains("trace")) &&
+ project.getComponents().stream().anyMatch(x -> x.getClass().getName().toLowerCase().contains("pad"))) {
+ tags.add("PCB");
+ }
+ if (project.getComponents().stream().anyMatch(x -> x.getClass().getName().toLowerCase().contains("tube"))) {
+ tags.add("tube");
+ }
+ if (project.getComponents().stream().anyMatch(x -> x.getClass().getName().toLowerCase().contains("vero"))) {
+ tags.add("vero/strip");
+ }
+ return tags;
+ }
+
+ private static final Set> PROPERTY_TYPES_TO_SKIP = Set.of(Font.class, Color.class);
+ private static final Set PROPERTY_NAMES_TO_SKIP = Set.of("alpha");
+
+ static AiSwitch mapSwitch(ISwitch sw) {
+ List positions = new ArrayList<>();
+ IDIYComponent> c = (IDIYComponent>) sw;
+ for (int p = 0; p < sw.getPositionCount(); p++) {
+ List> connections = new ArrayList<>();
+ for (int i = 0; i < c.getControlPointCount() - 1; i++) {
+ for (int j = i + 1; j < c.getControlPointCount(); j++) {
+ if (sw.arePointsConnected(i, j, p)) {
+ connections.add(Set.of(i, j));
+ }
+ }
+ }
+ positions.add(new AiSwitchPosition(sw.getPositionName(p), connections));
+ }
+ return new AiSwitch(c.getName(), positions);
+ }
+
+ static AiComponent mapComponent(IDIYComponent> component) {
+ ComponentType componentType = ComponentProcessor.getInstance()
+ .extractComponentTypeFrom((Class extends IDIYComponent>>) component.getClass());
+ List properties =
+ ComponentProcessor.getInstance().extractProperties(component.getClass());
+
+ AiPoint pos = null;
+ AiPoint fromPos = null;
+ AiPoint toPos = null;
+// Map componentDescriptorMap = new HashMap<>();
+// if (!component.isControlPointSticky(0)) {
+// Point2D controlPoint1 = component.getControlPoint(0);
+// if (component.getControlPointCount() > 1 && !component.isControlPointSticky(component.getControlPointCount() - 1)) {
+// Point2D controlPoint2 = component.getControlPoint(component.getControlPointCount() - 1);
+// fromPos = new AiPoint((int)Math.round(controlPoint1.getX()), (int)Math.round(controlPoint1.getY()));
+// toPos = new AiPoint((int)Math.round(controlPoint2.getX()), (int)Math.round(controlPoint2.getY()));
+// } else {
+// pos = new AiPoint((int)Math.round(controlPoint1.getX()), (int)Math.round(controlPoint1.getY()));
+// }
+// }
+//
+// if (component.getValue() != null && !PROPERTY_TYPES_TO_SKIP.contains(component.getValue().getClass())) {
+// componentDescriptorMap.put("value", component.getValue().toString());
+// }
+
+// properties.forEach(p -> {
+// if (PROPERTY_TYPES_TO_SKIP.contains(p.getType()))
+// return;
+// if (PROPERTY_NAMES_TO_SKIP.contains(p.getName().toLowerCase()))
+// return;
+//
+// try {
+// p.readFrom(component);
+// if (p.getValue() == null)
+// return;
+//
+// componentDescriptorMap.put(p.getName(), p.getValue().toString());
+// } catch (Exception e) {
+// LOG.warn("Error extracting properties", e);
+// }
+// });
+
+ List terminals = new ArrayList<>();
+
+ for (int i = 0; i < component.getControlPointCount(); i++) {
+ if (component.isControlPointSticky(i)) {
+ Point2D controlPoint = component.getControlPoint(i);
+ String nodeName = component.getControlPointNodeName(i);
+ AiTerminal terminal = new AiTerminal(i, Integer.toString(i+1).equals(nodeName) ? null : nodeName,
+ new AiPoint((int)Math.round(controlPoint.getX()),
+ (int)Math.round(controlPoint.getY())));
+ terminals.add(terminal);
+ }
+ }
+
+ return new AiComponent(component.getName(), componentType.getName(),
+ component.getValue() == null ? null : component.getValue().toString(),
+ fromPos, toPos, pos, terminals.isEmpty() ? null : terminals);
+ }
+
+ static String mapTerminal(NetlistBuilder.TerminalRef terminalRef) {
+ return terminalRef.component.getName() + "." + terminalRef.terminalIndex;
+ }
+}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/ChatbotService.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/ChatbotService.java
index 0f84a793..78936d54 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/ChatbotService.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/ChatbotService.java
@@ -23,6 +23,8 @@ package org.diylc.plugins.chatbot.service;
import com.diyfever.httpproxy.PhpFlatProxy;
import com.diyfever.httpproxy.ProxyFactory;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.ConfigurationManager;
@@ -34,13 +36,13 @@ import org.diylc.common.PropertyWrapper;
import org.diylc.core.IDIYComponent;
import org.diylc.core.ISwitch;
import org.diylc.netlist.*;
-import org.diylc.plugins.chatbot.model.ChatMessageEntity;
-import org.diylc.plugins.chatbot.model.IChatbotAPI;
-import org.diylc.plugins.chatbot.model.SubscriptionEntity;
+import org.diylc.plugins.chatbot.model.*;
import org.diylc.plugins.cloud.model.IServiceAPI;
import org.diylc.plugins.cloud.service.NotLoggedInException;
import org.diylc.presenter.ComponentProcessor;
+import org.diylc.presenter.ContinuityArea;
import org.diylc.utils.FileUtils;
+import org.diylc.utils.ReflectionUtils;
import java.awt.*;
import java.io.File;
@@ -48,7 +50,6 @@ import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
import java.util.List;
-import java.util.stream.Collectors;
import static org.diylc.utils.FileUtils.extractFileName;
@@ -59,11 +60,11 @@ import static org.diylc.utils.FileUtils.extractFileName;
*/
public class ChatbotService {
- public static final String LOG_IN_MESSAGE =
- "Please log into your DIYLC Cloud account in order to use the AI Assistant";
-
private final static Logger LOG = Logger.getLogger(ChatbotService.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
+ static {
+ MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ }
public static final String WELCOME_MESSAGE = "Welcome to the DIYLC AI Assistant!";
private final IPlugInPort plugInPort;
@@ -79,14 +80,14 @@ public class ChatbotService {
@Override
public EnumSet getSubscribedEventTypes() {
- return EnumSet.of(EventType.PROJECT_LOADED, EventType.FILE_STATUS_CHANGED);
+ return EnumSet.of(EventType.PROJECT_LOADED, EventType.PROJECT_SAVED);
}
@Override
public void processMessage(EventType eventType, Object... objects) {
if (eventType == EventType.PROJECT_LOADED) {
ChatbotService.this.currentProjectName = extractFileName((String)objects[2]);
- } else if (eventType == EventType.FILE_STATUS_CHANGED) {
+ } else if (eventType == EventType.PROJECT_SAVED) {
String newProjectName = extractFileName((String)objects[0]);
if (!Objects.equals(ChatbotService.this.currentProjectName, newProjectName)) {
if (plugInPort.getCloudService().isLoggedIn()) {
@@ -117,24 +118,68 @@ public class ChatbotService {
if (!plugInPort.getCloudService().isLoggedIn())
throw new NotLoggedInException();
+ LOG.info("Prompting chatbot: " + prompt);
+
String currentFile = plugInPort.getCurrentFileName();
String fileName = extractFileName(currentFile);
- String netlist;
- File netlistFile = null;
+ File aiProjectFile = null;
try {
- netlist = extractNetlists();
- if (netlist != null) {
- netlistFile = File.createTempFile("diylc_netlist_", ".txt");
- try (FileWriter writer = new FileWriter(netlistFile)) {
- writer.write(netlist);
+ AiProject aiProject = extractAiProject();
+ String aiProjectStr = null;
+ try {
+ aiProjectStr = MAPPER.writeValueAsString(aiProject);
+ } catch (JsonProcessingException e) {
+ LOG.error("Could not serialize aiProject to JSON", e);
+ }
+ if (aiProjectStr != null) {
+ aiProjectFile = File.createTempFile("diylc_netlist_", ".txt");
+ try (FileWriter writer = new FileWriter(aiProjectFile)) {
+ writer.write("""
+ Take these notes into account when analyzing the circuit
+ - Coordinates are represented in pixels assuming 200px/in resolution.
+ - Each component will either have a set of terminals that are connectable to other components or one or two points defining its position.
+ - Netlists of connected terminals are defined in the 'nets' structure.
+ - Analyze labels - they could provide clues about nearby terminals and what they represent.
+ """);
+ if (aiProject.switches() != null && !aiProject.switches().isEmpty()) {
+ writer.write("""
+ - Switches are defined in the 'switches' structure. Each switch will have a list of available position and the collection of internal terminals that are connected in each of the positions. Take the switching matrix into consideration when analyzing the circuit operation.
+ """);
+ }
+ if (aiProject.tags().contains("guitar")) {
+ IGuitarDiagramAnalyzer guitarDiagramAnalyzer =
+ ReflectionUtils.getGuitarDiagramAnalyzer();
+ if (guitarDiagramAnalyzer != null) {
+ try {
+ List netlists = plugInPort.extractNetlists(true);
+ writer.write("""
+ - The circuit represents a guitar wiring diagram and below are some of the known characteristics in each combination of switch positions:
+ """);
+ netlists.forEach(netlist -> {
+ List notes = guitarDiagramAnalyzer.collectNotes(netlist);
+ try {
+ writer.write(netlist.getSwitchSetupString() + ": \n");
+ for (String note : notes) {
+ writer.write(" - " + note + "\n");
+ }
+ } catch (IOException ex) {
+ LOG.warn("Could not write guitar diagram notes", ex);
+ }
+ });
+ } catch (NetlistException | IOException ex) {
+ LOG.warn("Could not extract guitar diagram netlist", ex);
+ }
+ }
+ }
+ writer.write("\nBelow is the JSON describing the circuit in detail.\n");
+ writer.write(aiProjectStr);
}
// Make sure the temp file is deleted when the JVM exits
- netlistFile.deleteOnExit();
+ aiProjectFile.deleteOnExit();
}
- } catch (NetlistException | IOException e) {
+ } catch (Exception e) {
LOG.error("Error extracting or saving netlist", e);
- netlist = null;
}
try {
@@ -143,117 +188,19 @@ public class ChatbotService {
plugInPort.getCloudService().getCurrentToken(),
plugInPort.getCloudService().getMachineId(),
fileName,
- netlistFile,
+ aiProjectFile,
prompt);
} finally {
// Clean up the temp file immediately after use
- if (netlistFile != null && netlistFile.exists()) {
- netlistFile.delete();
+ if (aiProjectFile != null && aiProjectFile.exists()) {
+ aiProjectFile.delete();
}
}
}
- private static final Set> PROPERTY_TYPES_TO_SKIP = Set.of(Font.class, Color.class);
-
- private String extractNetlists() throws NetlistException {
- List netlists = plugInPort.extractNetlists(true);
-
- if (netlists == null || netlists.isEmpty()) {
- return null;
- }
-
- StringBuilder sb = new StringBuilder();
-
-// Set extends IDIYComponent>> components = netlists.stream()
-// .flatMap(n -> n.getGroups().stream()
-// .flatMap(g -> g.getNodes().stream().
-// map(Node::getComponent)))
-// .collect(Collectors.toSet());
-//
-// if (!components.isEmpty()) {
-// sb.append(
-// "Components (each element of the JSON array represents one component with all the relevant properties): ");
-// outputComponents(components, sb);
-// }
-
- Set extends ISwitch> switches = plugInPort.getCurrentProject().getComponents()
- .stream().filter(x -> x instanceof ISwitch)
- .map(x -> (ISwitch)x)
- .collect(Collectors.toSet());;
-
- if (!switches.isEmpty()) {
- sb.append(
- "Switches (each row represents one switch, starts with the switch name and lists key properties of a switch):\n\n");
- outputSwitches(switches, sb);
- }
-
- for (Netlist netlist : netlists) {
- sb.append("\n");
- if (!netlist.getSwitchSetup().isEmpty()) {
- sb.append("Switch configuration: ").append(netlist.getSwitchSetup()).append(". ");
- }
- sb.append("Circuit netlist")
- .append(netlist.getSwitchSetup().isEmpty() ? "" : " in this switch configuration")
- .append(":\n\n");
- try {
- sb.append(SpiceSumarizer.summarize(netlist, false));
- } catch (TreeException e) {
- LOG.error("Error exporting netlist to Spice", e);
- }
- }
- return sb.toString();
- }
-
- private static void outputSwitches(Set extends ISwitch> switches, StringBuilder sb) {
- for (ISwitch sw : switches) {
- IDIYComponent> component = (IDIYComponent>) sw;
- ComponentType componentType = ComponentProcessor.getInstance()
- .extractComponentTypeFrom((Class extends IDIYComponent>>) component.getClass());
- sb.append(component.getName())
- .append(" ").append(component.getValueForDisplay()).append(" ")
- .append(componentType.getName())
- .append(" [").append(componentType.getCategory()).append("]\n");
- }
- }
-
- private static void outputComponents(Set extends IDIYComponent>> components, StringBuilder sb) {
- List