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>) 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> 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 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 switches, StringBuilder sb) { - for (ISwitch sw : switches) { - IDIYComponent component = (IDIYComponent) sw; - ComponentType componentType = ComponentProcessor.getInstance() - .extractComponentTypeFrom((Class>) 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> components, StringBuilder sb) { - List> componentDescriptors = new ArrayList<>(); - - components.forEach(c -> { - ComponentType componentType = ComponentProcessor.getInstance() - .extractComponentTypeFrom((Class>) c.getClass()); - List properties = - ComponentProcessor.getInstance().extractProperties(c.getClass()); - - Map componentDescriptorMap = new HashMap<>(); - componentDescriptorMap.put("name", c.getName()); - componentDescriptorMap.put("type", componentType.getName()); - componentDescriptorMap.put("category", componentType.getCategory()); - - properties.forEach(p -> { - if (PROPERTY_TYPES_TO_SKIP.contains(p.getType())) - return; - - try { - p.readFrom(c); - if (p.getValue() == null) - return; - - componentDescriptorMap.put(p.getName(), p.getValue().toString()); - } catch (Exception e) { - LOG.warn("Error extracting properties", e); - } - }); - - componentDescriptors.add(componentDescriptorMap); - }); - - try { - String json = MAPPER.writeValueAsString(componentDescriptors); - sb.append(json).append("\n\n"); - } catch (Exception e) { - LOG.error("Failed to serialize component descriptors to JSON", e); - } + private AiProject extractAiProject() throws NetlistException { + List continuityAreas = plugInPort.getDrawingManager().getContinuityAreas(); + return AiProjectBuilder.build(plugInPort.getCurrentProject(), continuityAreas); } public SubscriptionEntity getSubscriptionInfo() throws NotLoggedInException { @@ -317,23 +264,23 @@ public class ChatbotService { public static final String[] diylcQuestions = { "How do I add a component to my layout in DIYLC?", - "How can I quickly find a specific component in DIYLC?s component toolbox?", + "How can I quickly find a specific component in DIYLC's component toolbox?", "How do I move or reposition components on the canvas in DIYLC?", - "How do I edit or change a component?s properties (e.g. value or color) in DIYLC?", + "How do I edit or change a component's properties (e.g. value or color) in DIYLC?", "Is there a shortcut to duplicate or repeat the last component I placed in DIYLC?", "How can I group multiple components so I can move or edit them together in DIYLC?", - "What are ?building blocks? in DIYLC and how do I create or use them?", + "What are 'building blocks' in DIYLC and how do I create or use them?", "How do I save my DIYLC project and reopen it later on?", "How do I export my DIYLC layout as an image or PDF file?", "Can DIYLC generate a bill of materials (BOM) for my project, and how would I do that?", "How can I change or hide different layers in a DIYLC project to manage overlapping components?", "Is it possible to view the underside of a board in DIYLC, and how can I enable that?", - "What does the ?Highlight Connected Areas? feature do in DIYLC and how do I use it for debugging?", - "How can I analyze a guitar wiring diagram using DIYLC?s built-in tools?", + "What does the 'Highlight Connected Areas' feature do in DIYLC and how do I use it for debugging?", + "How can I analyze a guitar wiring diagram using DIYLC's built-in tools?", "How do I assign a custom keyboard shortcut to a component in DIYLC for quick access?", "What are component variants in DIYLC and how can I create and apply them?", "What is the continuous creation mode in DIYLC and how does it help when adding many components?", - "How can I create a custom component to add to DIYLC?s component library?", + "How can I create a custom component to add to DIYLC's component library?", "Does DIYLC support plug-ins, and how can I add new functionality to the app with them?", "What is the DIYLC cloud feature and how can I share or download projects using it?" }; diff --git a/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/NetlistBuilder.java b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/NetlistBuilder.java new file mode 100644 index 00000000..cb4ac1fa --- /dev/null +++ b/diylc/diylc-core/src/main/java/org/diylc/plugins/chatbot/service/NetlistBuilder.java @@ -0,0 +1,137 @@ +package org.diylc.plugins.chatbot.service; + +import org.diylc.core.IContinuity; +import org.diylc.core.IDIYComponent; +import org.diylc.core.ISwitch; +import org.diylc.presenter.ContinuityArea; + +import java.awt.geom.Area; +import java.awt.geom.Point2D; +import java.util.*; + +public class NetlistBuilder { + + public static class TerminalRef { + public final IDIYComponent component; + public final int terminalIndex; + + public TerminalRef(IDIYComponent component, int terminalIndex) { + this.component = component; + this.terminalIndex = terminalIndex; + } + + public Point2D getPosition() { + return component.getControlPoint(terminalIndex); + } + + @Override + public String toString() { + return component.getName() + "." + terminalIndex; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof TerminalRef)) return false; + TerminalRef other = (TerminalRef) obj; + return Objects.equals(component, other.component) && terminalIndex == other.terminalIndex; + } + + @Override + public int hashCode() { + return Objects.hash(component, terminalIndex); + } + } + + public static List> buildNets(List> components, List continuityAreas) { + Map terminalIndex = new HashMap<>(); + List allTerminals = new ArrayList<>(); + int idx = 0; + + for (IDIYComponent c : components) { + for (int i = 0; i < c.getControlPointCount(); i++) { + if (c.isControlPointSticky(i)) { + TerminalRef ref = new TerminalRef(c, i); + terminalIndex.put(ref, idx++); + allTerminals.add(ref); + } + } + } + + UnionFind uf = new UnionFind(allTerminals.size()); + + // Step 1: connect conductive components + for (IDIYComponent c : components) { + if (c instanceof IContinuity continuity && !(c instanceof ISwitch)) { + int count = c.getControlPointCount(); + for (int i = 0; i < count; i++) { + for (int j = i + 1; j < count; j++) { + if (continuity.arePointsConnected(i, j)) { + TerminalRef a = new TerminalRef(c, i); + TerminalRef b = new TerminalRef(c, j); + uf.union(terminalIndex.get(a), terminalIndex.get(b)); + } + } + } + } + } + + // Step 2: connect terminals inside continuity areas + for (ContinuityArea continuity : continuityAreas) { + Area area = continuity.getSimplifiedArea(); + List inside = new ArrayList<>(); + + for (TerminalRef t : allTerminals) { + Point2D p = t.getPosition(); + if (area.contains(p)) { + inside.add(t); + } + } + + for (int i = 0; i < inside.size(); i++) { + for (int j = i + 1; j < inside.size(); j++) { + uf.union(terminalIndex.get(inside.get(i)), terminalIndex.get(inside.get(j))); + } + } + } + + Map> netMap = new HashMap<>(); + for (TerminalRef t : allTerminals) { + int root = uf.find(terminalIndex.get(t)); + netMap.computeIfAbsent(root, k -> new HashSet<>()).add(t); + } + + // Filter out nets with only one terminal and remove exact duplicates + List> nets = new ArrayList<>(); + Set> seen = new HashSet<>(); + + for (Set net : netMap.values()) { + if (net.size() > 1 && seen.add(net)) { + nets.add(net); + } + } + return nets; + + } + + // Union-Find with path compression + static class UnionFind { + int[] parent; + + UnionFind(int n) { + parent = new int[n]; + for (int i = 0; i < n; i++) parent[i] = i; + } + + int find(int x) { + if (parent[x] != x) + parent[x] = find(parent[x]); + return parent[x]; + } + + void union(int x, int y) { + parent[find(x)] = find(y); + } + } +} + diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java index c43afe2c..1ee5f117 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java @@ -2831,7 +2831,13 @@ public class Presenter implements IPlugInPort { return chatbotService; } + @Override public MessageDispatcher getMessageDispatcher() { return messageDispatcher; } + + @Override + public DrawingManager getDrawingManager() { + return drawingManager; + } } diff --git a/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java b/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java index 380646af..f9cb406e 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java +++ b/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java @@ -177,6 +177,7 @@ public class ProjectFileManager { if (!isBackup) { this.currentFileName = fileName; this.modified = false; + messageDispatcher.dispatchMessage(EventType.PROJECT_SAVED, getCurrentFileName()); fireFileStatusChanged(); } } diff --git a/diylc/diylc-core/src/main/java/org/diylc/utils/ReflectionUtils.java b/diylc/diylc-core/src/main/java/org/diylc/utils/ReflectionUtils.java index e797334f..7b3e2d12 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/utils/ReflectionUtils.java +++ b/diylc/diylc-core/src/main/java/org/diylc/utils/ReflectionUtils.java @@ -31,6 +31,7 @@ import org.apache.log4j.Logger; import org.diylc.appframework.miscutils.Utils; import org.diylc.core.annotations.IAutoCreator; +import org.diylc.netlist.IGuitarDiagramAnalyzer; import org.diylc.netlist.INetlistParser; import org.diylc.presenter.Presenter; @@ -89,4 +90,25 @@ public class ReflectionUtils { return null; } } + + public static IGuitarDiagramAnalyzer getGuitarDiagramAnalyzer() throws Exception { + Set> classes; + classes = Utils.getClasses("org.diylc.netlist"); + List result = new ArrayList<>(); + + for (Class clazz : classes) { + if (!Modifier.isAbstract(clazz.getModifiers()) && IGuitarDiagramAnalyzer.class.isAssignableFrom(clazz)) { + result.add((IGuitarDiagramAnalyzer) clazz.getDeclaredConstructor().newInstance()); + } + } + + + if (result.isEmpty()) { + return null; + } + if (result.size() > 1) { + throw new RuntimeException("Found more than one IGuitarDiagramAnalyzer"); + } + return result.get(0); + } } diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml index 795f5d5f..54f2ff31 100644 --- a/diylc/diylc-core/src/main/resources/update.xml +++ b/diylc/diylc-core/src/main/resources/update.xml @@ -258,9 +258,13 @@ 3 0 - 2025-03-27 00:00:00.000 CET + 2025-04-21 00:00:00.000 CET + + IMPROVEMENT + Improved AI Assistant quality of responses, especially when analyzing a circuit + IMPROVEMENT When saving a project to the new file, retain all the AI Assistant chat history for the new project file @@ -275,7 +279,11 @@ IMPROVEMENT - Output current subscription statistics at the end of the chat for transparency + Output current subscription statistics at the end of the chat for usage transparency + + + IMPROVEMENT + Improved styling and usability of chatbot tool BUG_FIX diff --git a/diylc/diylc-library/pom.xml b/diylc/diylc-library/pom.xml index 0430a40b..6430f6de 100644 --- a/diylc/diylc-library/pom.xml +++ b/diylc/diylc-library/pom.xml @@ -7,7 +7,7 @@ org.diylc diylc-parent - 5.2.0 + 5.3.0 diylc-library diff --git a/diylc/diylc-library/src/main/java/org/diylc/netlist/GuitarDiagramAnalyzer.java b/diylc/diylc-library/src/main/java/org/diylc/netlist/GuitarDiagramAnalyzer.java index 0398f0fb..6bd5caef 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/netlist/GuitarDiagramAnalyzer.java +++ b/diylc/diylc-library/src/main/java/org/diylc/netlist/GuitarDiagramAnalyzer.java @@ -57,7 +57,7 @@ import org.diylc.netlist.TreeException; import org.diylc.netlist.TreeLeaf; import org.diylc.netlist.Tree.ITreeWalker; -public class GuitarDiagramAnalyzer extends AbstractNetlistAnalyzer implements INetlistAnalyzer { +public class GuitarDiagramAnalyzer extends AbstractNetlistAnalyzer implements INetlistAnalyzer, IGuitarDiagramAnalyzer { private static Set JACK_TYPES = new HashSet(); private static Set POT_TYPES = new HashSet(); @@ -119,6 +119,27 @@ public class GuitarDiagramAnalyzer extends AbstractNetlistAnalyzer implements IN return Set.of(NetlistSwitchPreference.WITH); } + public List collectNotes(Netlist netlist) { + + List notes = new ArrayList(); + + Tree tree = null; + try { + tree = constructTree(netlist); + } catch (TreeException e) { + throw new RuntimeException(e); + } + + if (tree == null) { + notes.add("No pickups are connected to the output jack"); + return notes; + } + + processTree(tree, notes); + + return notes; + } + protected Summary summarize(Netlist netlist) throws TreeException { List notes = new ArrayList(); diff --git a/diylc/diylc-swing/pom.xml b/diylc/diylc-swing/pom.xml index 67ce10f2..5526fe46 100644 --- a/diylc/diylc-swing/pom.xml +++ b/diylc/diylc-swing/pom.xml @@ -7,7 +7,7 @@ org.diylc diylc-parent - 5.2.0 + 5.3.0 diylc-swing diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/chatbot/ChatbotPane.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/chatbot/ChatbotPane.java index 8940c2fb..1faa7512 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/chatbot/ChatbotPane.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/chatbot/ChatbotPane.java @@ -16,6 +16,8 @@ import javax.swing.*; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.util.List; import java.util.Objects; import java.util.Random; @@ -34,6 +36,10 @@ public class ChatbotPane extends JPanel { private static final Font MONOSPACED_FONT = new Font("Monospaced", Font.PLAIN, 12); private static final Color TERMINAL_BG = new Color(40, 40, 40); private static final Color TERMINAL_FG = new Color(200, 200, 200); + private static final Color BUTTON_BG = new Color(70, 70, 70); // Brighter background for buttons + private static final Color BUTTON_FG = new Color(240, 240, 240); // Bright text for contrast + private static final Color BUTTON_BORDER = new Color(100, 100, 100); // Subtle border + private static final Color BUTTON_HOVER = new Color(90, 90, 90); // Hover state color public static final String HTML_STYLE = String.format( "