Cloud login improvements

This commit is contained in:
Bane Stojkovic
2025-04-05 10:36:55 +02:00
parent d097bcd740
commit a5cc9c1374
5 changed files with 161 additions and 114 deletions
@@ -111,7 +111,7 @@ public class ChatbotService {
if (netlists == null) { if (netlists == null) {
return null; return null;
} }
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("Components: "); sb.append("Components: ");
@@ -169,25 +169,6 @@ public class ChatbotService {
return sb.toString(); return sb.toString();
} }
public String getInitialChatContents() {
Random r = new Random();
String chatHtml =
"<div class='system'>" + WELCOME_MESSAGE + "</div><br><br>\n" +
"<div class='assistant'>I'm here to help you design, build, and troubleshoot your electronics projects using DIY Layout Creator (DIYLC). You can ask me questions about:</div><br>\n" +
// "<ul>\n" +
"\n" +
"- <b>Electronics theory or concepts:</b> <i>e.g.,</i> '<span class='user'>" + electronicsQuestions[r.nextInt(
electronicsQuestions.length)] + "</span>'<br>\n" +
"- <b>DIYLC features and usage:</b> <i>e.g.,</i> '<span class='user'>" + diylcQuestions[r.nextInt(diylcQuestions.length)] + "</span>'<br>\n" +
"- <b>Your current circuit project:</b> <i>e.g.,</i> '<span class='user'>" + circuitQuestions[r.nextInt(circuitQuestions.length)] + "</span>'<br>\n" +
// "</ul><br>\n" +
"<br>\n" +
"<div class='assistant'>Feel free to ask your own question or use one of these examples to get started!</div><br>\n";
return chatHtml;
}
public SubscriptionEntity getSubscriptionInfo() throws NotLoggedInException { public SubscriptionEntity getSubscriptionInfo() throws NotLoggedInException {
if (!plugInPort.getCloudService().isLoggedIn()) if (!plugInPort.getCloudService().isLoggedIn())
throw new NotLoggedInException(); throw new NotLoggedInException();
@@ -219,7 +200,7 @@ public class ChatbotService {
public static final String USER = "user"; public static final String USER = "user";
public static final String TEMPORARY = "temporary"; public static final String TEMPORARY = "temporary";
String[] electronicsQuestions = { public static final String[] electronicsQuestions = {
"What is Ohm's Law, and how do voltage, current, and resistance relate to each other?", "What is Ohm's Law, and how do voltage, current, and resistance relate to each other?",
"Why are resistors important in circuits, and what happens if the resistor value is incorrect?", "Why are resistors important in circuits, and what happens if the resistor value is incorrect?",
"What is a capacitor, and how does it behave differently from a resistor?", "What is a capacitor, and how does it behave differently from a resistor?",
@@ -247,7 +228,7 @@ public class ChatbotService {
"How can you determine if components in your circuit are properly rated for the voltage and current they're experiencing?" "How can you determine if components in your circuit are properly rated for the voltage and current they're experiencing?"
}; };
String[] diylcQuestions = { public static final String[] diylcQuestions = {
"How do I add a component to my layout in DIYLC?", "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 move or reposition components on the canvas in DIYLC?",
@@ -270,7 +251,7 @@ public class ChatbotService {
"What is the DIYLC cloud feature and how can I share or download projects using it?" "What is the DIYLC cloud feature and how can I share or download projects using it?"
}; };
String[] circuitQuestions = { public static final String[] circuitQuestions = {
"What's the purpose of this specific resistor in my circuit?", "What's the purpose of this specific resistor in my circuit?",
"How do I correctly determine capacitor values for my power supply?", "How do I correctly determine capacitor values for my power supply?",
"How should I route wiring to reduce interference and noise?", "How should I route wiring to reduce interference and noise?",
@@ -21,9 +21,8 @@
*/ */
package org.diylc.plugins.cloud.service; package org.diylc.plugins.cloud.service;
import java.io.File; import java.io.*;
import java.io.FileOutputStream; import java.lang.management.RuntimeMXBean;
import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.NetworkInterface; import java.net.NetworkInterface;
import java.net.URL; import java.net.URL;
@@ -33,10 +32,12 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.ConfigurationManager; import org.diylc.appframework.miscutils.ConfigurationManager;
import java.lang.management.ManagementFactory;
import com.diyfever.httpproxy.PhpFlatProxy; import com.diyfever.httpproxy.PhpFlatProxy;
import com.diyfever.httpproxy.ProxyFactory; import com.diyfever.httpproxy.ProxyFactory;
import org.diylc.appframework.miscutils.Utils;
import org.diylc.common.EventType; import org.diylc.common.EventType;
import org.diylc.common.IPlugInPort; import org.diylc.common.IPlugInPort;
import org.diylc.common.PropertyWrapper; import org.diylc.common.PropertyWrapper;
@@ -47,6 +48,14 @@ import org.diylc.plugins.cloud.model.UserEntity;
import org.diylc.presenter.ComparatorFactory; import org.diylc.presenter.ComparatorFactory;
import org.diylc.presenter.ComponentProcessor; import org.diylc.presenter.ComponentProcessor;
import javax.swing.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Enumeration;
/** /**
* Contains all the back-end logic for using the cloud and manipulating projects on the cloud. * Contains all the back-end logic for using the cloud and manipulating projects on the cloud.
* *
@@ -54,17 +63,21 @@ import org.diylc.presenter.ComponentProcessor;
*/ */
public class CloudService { public class CloudService {
private static String USERNAME_KEY = "cloud.Username"; private static final Logger LOG = Logger.getLogger(CloudService.class);
private static String TOKEN_KEY = "cloud.token"; private static final String MACHINE_ID_FILE = "machine.id";
private static final String MACHINE_ID_KEY = "cloud.machineId";
private static String ERROR = "Error"; private static final String USERNAME_KEY = "cloud.Username";
private static final String TOKEN_KEY = "cloud.token";
private final static Logger LOG = Logger.getLogger(CloudService.class); private static final String ERROR = "Error";
private static final Object SUCCESS = "Success"; private static final Object SUCCESS = "Success";
private static final ExecutorService executor = Executors.newSingleThreadExecutor();
private final IPlugInPort plugInPort; private final IPlugInPort plugInPort;
private IServiceAPI service; private IServiceAPI service;
private String serviceUrl; private String serviceUrl;
private UserEntity currentUser;
private String currentToken;
private String machineId; private String machineId;
private String[] categories; private String[] categories;
@@ -72,6 +85,22 @@ public class CloudService {
public CloudService(IPlugInPort plugInPort) { public CloudService(IPlugInPort plugInPort) {
this.plugInPort = plugInPort; this.plugInPort = plugInPort;
serviceUrl = ConfigurationManager.getInstance().readString(IServiceAPI.URL_KEY,
"http://www.diy-fever.com/diylc/api/v1");
ProxyFactory factory = new ProxyFactory(new PhpFlatProxy());
service = factory.createProxy(IServiceAPI.class, serviceUrl);
// Delay a test of logged in state to give it time to complete
Timer timer = new Timer(1000, e -> {
try {
tryLogInWithToken();
} catch (Exception ex) {
LOG.error("Background login attempt failed", ex);
}
});
timer.setRepeats(false); // Make it one-shot
timer.start();
} }
private IServiceAPI getService() { private IServiceAPI getService() {
@@ -107,7 +136,7 @@ public class CloudService {
} }
} }
public boolean tryLogInWithToken() throws CloudException { private void tryLogInWithToken() throws CloudException {
String username = ConfigurationManager.getInstance().readString(USERNAME_KEY, null); String username = ConfigurationManager.getInstance().readString(USERNAME_KEY, null);
String token = ConfigurationManager.getInstance().readString(TOKEN_KEY, null); String token = ConfigurationManager.getInstance().readString(TOKEN_KEY, null);
@@ -121,22 +150,19 @@ public class CloudService {
} }
if (res == null || res.equals(ERROR)) { if (res == null || res.equals(ERROR)) {
LOG.info("Login failed"); LOG.info("Login failed");
return false;
} else { } else {
LOG.info("Login success"); LOG.info("Login success");
this.loggedIn = true; this.loggedIn = true;
this.plugInPort.getMessageDispatcher().dispatchMessage(EventType.CLOUD_LOGGED_IN); this.plugInPort.getMessageDispatcher().dispatchMessage(EventType.CLOUD_LOGGED_IN);
return true;
} }
} else }
return false;
} }
public void logOut() { public void logOut() {
LOG.info("Logged out"); LOG.info("Logged out");
ConfigurationManager.getInstance().writeValue(TOKEN_KEY, null); ConfigurationManager.getInstance().writeValue(TOKEN_KEY, null);
this.plugInPort.getMessageDispatcher().dispatchMessage(EventType.CLOUD_LOGGED_OUT);
this.loggedIn = false; this.loggedIn = false;
this.plugInPort.getMessageDispatcher().dispatchMessage(EventType.CLOUD_LOGGED_OUT);
} }
public boolean isLoggedIn() { public boolean isLoggedIn() {
@@ -428,22 +454,67 @@ public class CloudService {
public String getMachineId() { public String getMachineId() {
if (machineId == null) { if (machineId == null) {
try { if (Utils.isWindows()) {
InetAddress ip = InetAddress.getLocalHost(); machineId = System.getenv("COMPUTERNAME");
} else {
NetworkInterface network = NetworkInterface.getByInetAddress(ip); try {
Runtime r = Runtime.getRuntime();
byte[] mac = network.getHardwareAddress(); Process p = r.exec("uname -a");
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
StringBuilder sb = new StringBuilder(18); machineId = reader.readLine();
for (byte b : mac) { } catch (Exception e) {
if (sb.length() > 0) LOG.error("Error getting machineId from uname", e);
sb.append(':');
sb.append(String.format("%02x", b));
} }
}
if (machineId == null) {
try {
// Generate a unique identifier based on hardware information
List<String> identifiers = new ArrayList<>();
machineId = sb.toString(); // Get MAC addresses
} catch (Exception e) { Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface networkInterface = networkInterfaces.nextElement();
byte[] mac = networkInterface.getHardwareAddress();
if (mac != null) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mac.length; i++) {
sb.append(String.format("%02X", mac[i]));
}
identifiers.add(sb.toString());
}
}
// Get hostname
String hostname = InetAddress.getLocalHost().getHostName();
identifiers.add(hostname);
// Sort to ensure consistent order
Collections.sort(identifiers);
// Create a single string from all identifiers
StringBuilder combined = new StringBuilder();
for (String id : identifiers) {
combined.append(id);
}
// Create a hash of the combined string
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(combined.toString().getBytes());
// Convert to hex string
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
hexString.append(String.format("%02x", b));
}
machineId = hexString.toString();
} catch (Exception e) {
LOG.error("Failed to generate machine ID", e);
}
}
if (machineId == null) {
machineId = "Generic"; machineId = "Generic";
} }
} }
@@ -8,11 +8,13 @@ import org.diylc.plugins.chatbot.model.SubscriptionEntity;
import org.diylc.plugins.chatbot.service.ChatbotService; import org.diylc.plugins.chatbot.service.ChatbotService;
import org.diylc.plugins.cloud.service.NotLoggedInException; import org.diylc.plugins.cloud.service.NotLoggedInException;
import org.diylc.swing.ISwingUI; import org.diylc.swing.ISwingUI;
import org.diylc.utils.FileUtils;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Random;
public class ChatbotPane extends JPanel { public class ChatbotPane extends JPanel {
@@ -43,7 +45,7 @@ public class ChatbotPane extends JPanel {
private JButton clearButton; private JButton clearButton;
private boolean loggedIn = false; private boolean loggedIn = false;
private String projectFileName = null; private String projectFileName = "initial";
public ChatbotPane(ISwingUI swingUI, IPlugInPort plugInPort) { public ChatbotPane(ISwingUI swingUI, IPlugInPort plugInPort) {
super(); super();
@@ -202,7 +204,7 @@ public class ChatbotPane extends JPanel {
public void refreshChat() { public void refreshChat() {
boolean currentLoggedIn = plugInPort.getCloudService().isLoggedIn(); boolean currentLoggedIn = plugInPort.getCloudService().isLoggedIn();
String currentProjectFileName = plugInPort.getCurrentFileName(); String currentProjectFileName = FileUtils.extractFileName(plugInPort.getCurrentFileName());
if (this.loggedIn == currentLoggedIn && Objects.equals(currentProjectFileName, this.projectFileName)) if (this.loggedIn == currentLoggedIn && Objects.equals(currentProjectFileName, this.projectFileName))
return; return;
@@ -210,17 +212,26 @@ public class ChatbotPane extends JPanel {
this.loggedIn = currentLoggedIn; this.loggedIn = currentLoggedIn;
this.projectFileName = currentProjectFileName; this.projectFileName = currentProjectFileName;
resetChat(); clearChat();
try { if (!currentLoggedIn) {
SubscriptionEntity subscriptionInfo = plugInPort.getChatbotService().getSubscriptionInfo(); appendSection(ChatbotService.SYSTEM, "Please log into your cloud account to use the AI assistant");
appendSection(ChatbotService.SYSTEM, "Subscribed to " + subscriptionInfo.getTier() + getAskButton().setEnabled(false);
" tier with " + subscriptionInfo.getRemainingCredits() + " remaining credits."); getClearButton().setEnabled(false);
} catch (NotLoggedInException e) { } else {
LOG.error("Error getting subscription info", e); try {
appendSection(ChatbotService.SYSTEM, "Error getting subscription info"); SubscriptionEntity subscriptionInfo = plugInPort.getChatbotService().getSubscriptionInfo();
appendSection(ChatbotService.SYSTEM,
"Your current subscription tier is " + subscriptionInfo.getTier() +
" expiring on " + subscriptionInfo.getEndDate() + " with " +
subscriptionInfo.getRemainingCredits() + " credits remaining.");
} catch (NotLoggedInException e) {
LOG.error("Error getting subscription info", e);
appendSection(ChatbotService.SYSTEM, "Error getting subscription info");
}
fetchChatHistory();
getAskButton().setEnabled(true);
getClearButton().setEnabled(true);
} }
fetchChatHistory();
getAskButton().setEnabled(true);
} }
private void fetchChatHistory() { private void fetchChatHistory() {
@@ -250,13 +261,32 @@ public class ChatbotPane extends JPanel {
return chatEditorPane; return chatEditorPane;
} }
private void resetChat() { private void clearChat() {
String sampleChat = chatbotService.getInitialChatContents(); String sampleChat = getInitialChatContents();
chatEditorPane.setContentType("text/html"); chatEditorPane.setContentType("text/html");
chatEditorPane.setText(HTML_STYLE + "<body>" + sampleChat + "</body>"); chatEditorPane.setText(HTML_STYLE + "<body>" + sampleChat + "</body>");
} }
private String getInitialChatContents() {
Random r = new Random();
String chatHtml =
"<div class='system'>" + ChatbotService.WELCOME_MESSAGE + "</div><br><br>\n" +
"<div class='assistant'>I'm here to help you design, build, and troubleshoot your electronics projects using DIY Layout Creator (DIYLC). You can ask me questions about:</div><br>\n" +
// "<ul>\n" +
"\n" +
"- <b>Electronics theory or concepts:</b> <i>e.g.,</i> '<span class='user'>" +ChatbotService.electronicsQuestions[r.nextInt(
ChatbotService.electronicsQuestions.length)] + "</span>'<br>\n" +
"- <b>DIYLC features and usage:</b> <i>e.g.,</i> '<span class='user'>" + ChatbotService.diylcQuestions[r.nextInt(ChatbotService.diylcQuestions.length)] + "</span>'<br>\n" +
"- <b>Your current circuit project:</b> <i>e.g.,</i> '<span class='user'>" + ChatbotService.circuitQuestions[r.nextInt(ChatbotService.circuitQuestions.length)] + "</span>'<br>\n" +
// "</ul><br>\n" +
"<br>\n" +
"<div class='assistant'>Feel free to ask your own question or use one of these examples to get started!</div><br>\n";
return chatHtml;
}
public JButton getAskButton() { public JButton getAskButton() {
if (askButton == null) { if (askButton == null) {
askButton = new JButton("Ask"); askButton = new JButton("Ask");
@@ -337,7 +367,7 @@ public class ChatbotPane extends JPanel {
@Override @Override
public void complete(Void result) { public void complete(Void result) {
resetChat(); clearChat();
} }
}, false); }, false);
}); });
@@ -43,35 +43,18 @@ public class ChatbotPlugin implements IPlugIn {
public ChatbotPlugin(ISwingUI swingUI) { public ChatbotPlugin(ISwingUI swingUI) {
super(); super();
this.swingUI = swingUI; this.swingUI = swingUI;
// Delay a test of logged in state to give it time to complete
Timer timer = new Timer(2000, e -> {
if (!plugInPort.getCloudService().isLoggedIn()) {
getChatbotPane().refreshChat();
}
});
timer.setRepeats(false); // Make it one-shot
timer.start();
} }
@Override @Override
public EnumSet<EventType> getSubscribedEventTypes() { public EnumSet<EventType> getSubscribedEventTypes() {
return EnumSet.of(EventType.FILE_STATUS_CHANGED, EventType.PROJECT_LOADED, return EnumSet.of(EventType.FILE_STATUS_CHANGED, EventType.PROJECT_LOADED,
EventType.PROJECT_MODIFIED, EventType.SELECTION_CHANGED, EventType.CLOUD_LOGGED_IN, EventType.CLOUD_LOGGED_IN, EventType.CLOUD_LOGGED_OUT);
EventType.CLOUD_LOGGED_OUT);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public void processMessage(EventType eventType, Object... params) { public void processMessage(EventType eventType, Object... params) {
switch (eventType) { switch (eventType) {
case PROJECT_MODIFIED:
// getChatbotPane().setComponents(((Project) params[1]).getComponents(),
// plugInPort.getSelectedComponents());
break;
case SELECTION_CHANGED:
// getChatbotPane().setSelection((Set<IDIYComponent<?>>) params[0]);
break;
case FILE_STATUS_CHANGED: case FILE_STATUS_CHANGED:
case PROJECT_LOADED: case PROJECT_LOADED:
case CLOUD_LOGGED_IN: case CLOUD_LOGGED_IN:
@@ -108,33 +108,6 @@ public class CloudPlugIn implements IPlugIn {
public void connect(IPlugInPort plugInPort) { public void connect(IPlugInPort plugInPort) {
this.plugInPort = plugInPort; this.plugInPort = plugInPort;
this.cloudService = plugInPort.getCloudService(); this.cloudService = plugInPort.getCloudService();
initialize();
}
private void initialize() {
this.swingUI.executeBackgroundTask(new ITask<Boolean>() {
@Override
public Boolean doInBackground() throws Exception {
return cloudService.tryLogInWithToken();
}
@Override
public void failed(Exception e) {
LOG.error("Error while trying to login using token");
}
@Override
public void complete(Boolean result) {
try {
if (result)
loggedIn();
} catch (Exception e) {
LOG.error("Error while trying to login with token", e);
}
}
}, false);
} }
public CloudBrowserFrame getCloudBrowser() { public CloudBrowserFrame getCloudBrowser() {
@@ -206,11 +179,20 @@ public class CloudPlugIn implements IPlugIn {
@Override @Override
public EnumSet<EventType> getSubscribedEventTypes() { public EnumSet<EventType> getSubscribedEventTypes() {
return null; return EnumSet.of(EventType.CLOUD_LOGGED_IN, EventType.CLOUD_LOGGED_OUT);
} }
@Override @Override
public void processMessage(EventType eventType, Object... params) {} public void processMessage(EventType eventType, Object... params) {
switch (eventType) {
case CLOUD_LOGGED_IN:
loggedIn();
break;
case CLOUD_LOGGED_OUT:
loggedOut();
break;
}
}
class LibraryAction extends AbstractAction { class LibraryAction extends AbstractAction {