Add conversation history, fix reported bugs, visual improvements #9)

This commit is contained in:
Carl-Robert Linnupuu 2023-03-09 11:58:40 +00:00
parent 6246491668
commit f4cd93f018
53 changed files with 906 additions and 252 deletions

View file

@ -9,16 +9,16 @@ public enum BaseModel {
CHATGPT("gpt-3.5-turbo", "ChatGPT - Most recent and capable model (Default)"),
CHATGPT_SNAPSHOT("gpt-3.5-turbo-0301", "ChatGPT - Snapshot of gpt-3.5-turbo from March 1st 2023");
private final String model;
private final String code;
private final String description;
BaseModel(String model, String description) {
this.model = model;
BaseModel(String code, String description) {
this.code = code;
this.description = description;
}
public String getModel() {
return model;
public String getCode() {
return code;
}
public String getDescription() {

View file

@ -3,6 +3,7 @@ package ee.carlrobert.chatgpt.client;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ee.carlrobert.chatgpt.ide.conversations.Conversation;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import java.net.InetSocketAddress;
import java.net.Proxy;
@ -21,21 +22,36 @@ import okhttp3.sse.EventSources;
public abstract class Client {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ClientCode clientCode;
private EventSource eventSource;
protected String prompt = "";
protected Conversation conversation;
protected String prompt;
protected OkHttpClient client;
protected Client(ClientCode clientCode) {
this.clientCode = clientCode;
}
protected abstract ApiRequestDetails getRequestDetails(String prompt);
public abstract void clearPreviousSession();
protected abstract EventSourceListener getEventSourceListener(
Consumer<String> onMessageReceived,
Consumer<Conversation> onComplete,
Consumer<String> onFailure);
protected abstract EventSourceListener getEventSourceListener(Consumer<String> onMessageReceived, Runnable onComplete);
public void getCompletionsAsync(String prompt, Consumer<String> onMessageReceived, Runnable onComplete) {
public void getCompletionsAsync(
Conversation conversation,
String prompt,
Consumer<String> onMessageReceived,
Consumer<Conversation> onComplete,
Consumer<String> onFailure) {
this.conversation = conversation;
this.prompt = prompt;
this.client = buildClient();
this.eventSource = EventSources.createFactory(client)
.newEventSource(buildHttpRequest(prompt), getEventSourceListener(onMessageReceived, onComplete));
.newEventSource(
buildHttpRequest(prompt),
getEventSourceListener(onMessageReceived, onComplete, onFailure));
}
public OkHttpClient buildClient() {
@ -77,4 +93,8 @@ public abstract class Client {
throw new RuntimeException("Unable to serialize request payload", e);
}
}
public ClientCode getCode() {
return clientCode;
}
}

View file

@ -0,0 +1,8 @@
package ee.carlrobert.chatgpt.client;
public enum ClientCode {
CHAT_COMPLETIONS,
TEXT_COMPLETIONS,
UNOFFICIAL_CHATGPT
}

View file

@ -19,12 +19,18 @@ public abstract class CompletionClientEventListener extends EventSourceListener
private final OkHttpClient client;
private final Consumer<String> onMessageReceived;
private final Consumer<String> onComplete;
private final Consumer<String> onFailure;
private final StringBuilder messageBuilder = new StringBuilder();
public CompletionClientEventListener(OkHttpClient client, Consumer<String> onMessageReceived, Consumer<String> onComplete) {
public CompletionClientEventListener(
OkHttpClient client,
Consumer<String> onMessageReceived,
Consumer<String> onComplete,
Consumer<String> onFailure) {
this.client = client;
this.onMessageReceived = onMessageReceived;
this.onComplete = onComplete;
this.onFailure = onFailure;
}
protected abstract String getMessage(String data) throws JsonProcessingException;
@ -47,8 +53,10 @@ public abstract class CompletionClientEventListener extends EventSourceListener
}
var message = getMessage(data);
messageBuilder.append(message);
onMessageReceived.accept(message);
if (message != null) {
messageBuilder.append(message);
onMessageReceived.accept(message);
}
} catch (JsonProcessingException e) {
throw new RuntimeException("Unable to deserialize payload.", e);
}
@ -65,19 +73,17 @@ public abstract class CompletionClientEventListener extends EventSourceListener
try {
if (response == null) {
onMessageReceived.accept(DEFAULT_ERROR_MSG);
onFailure.accept(DEFAULT_ERROR_MSG);
return;
}
var body = response.body();
if (body != null) {
var error = new ObjectMapper().readValue(body.string(), ApiResponseError.class);
onMessageReceived.accept(error.getError().getMessage());
onFailure.accept(error.getError().getMessage());
}
} catch (IOException e) {
onMessageReceived.accept(DEFAULT_ERROR_MSG);
} finally {
onComplete.accept(messageBuilder.toString());
onFailure.accept(DEFAULT_ERROR_MSG);
}
}

View file

@ -2,21 +2,23 @@ package ee.carlrobert.chatgpt.client.official.chat;
import ee.carlrobert.chatgpt.client.ApiRequestDetails;
import ee.carlrobert.chatgpt.client.Client;
import ee.carlrobert.chatgpt.client.ClientCode;
import ee.carlrobert.chatgpt.client.official.chat.request.ApiRequest;
import ee.carlrobert.chatgpt.client.official.chat.request.ApiRequestMessage;
import ee.carlrobert.chatgpt.ide.conversations.Conversation;
import ee.carlrobert.chatgpt.ide.conversations.message.Message;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import okhttp3.sse.EventSourceListener;
public class ChatCompletionClient extends Client {
private static final List<Map.Entry<String, String>> queries = new ArrayList<>();
private static ChatCompletionClient instance;
private ChatCompletionClient() {
super(ClientCode.CHAT_COMPLETIONS);
}
public static ChatCompletionClient getInstance() {
@ -26,35 +28,38 @@ public class ChatCompletionClient extends Client {
return instance;
}
public void clearPreviousSession() {
queries.clear();
}
protected ApiRequestDetails getRequestDetails(String prompt) {
var messages = new ArrayList<ApiRequestMessage>();
messages.add(new ApiRequestMessage(
"system",
"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible."));
queries.forEach(query -> {
messages.add(new ApiRequestMessage("user", query.getKey()));
messages.add(new ApiRequestMessage("assistant", query.getValue()));
conversation.getMessages().forEach(message -> {
messages.add(new ApiRequestMessage("user", message.getPrompt()));
messages.add(new ApiRequestMessage("assistant", message.getResponse()));
});
messages.add(new ApiRequestMessage("user", prompt));
return new ApiRequestDetails(
"https://api.openai.com/v1/chat/completions",
new ApiRequest(
SettingsState.getInstance().chatCompletionBaseModel.getModel(),
SettingsState.getInstance().chatCompletionBaseModel.getCode(),
true,
messages
),
SettingsState.getInstance().apiKey);
}
protected EventSourceListener getEventSourceListener(Consumer<String> onMessageReceived, Runnable onComplete) {
protected EventSourceListener getEventSourceListener(
Consumer<String> onMessageReceived,
Consumer<Conversation> onComplete,
Consumer<String> onFailure) {
return new ChatCompletionClientEventListener(client, onMessageReceived, finalMessage -> {
queries.add(Map.entry(prompt, finalMessage));
onComplete.run();
});
var message = new Message();
message.setPrompt(prompt);
message.setResponse(finalMessage);
conversation.setUpdatedOn(LocalDateTime.now());
conversation.addMessage(message);
onComplete.accept(conversation);
}, onFailure);
}
}

View file

@ -9,8 +9,12 @@ import okhttp3.OkHttpClient;
public class ChatCompletionClientEventListener extends CompletionClientEventListener {
public ChatCompletionClientEventListener(OkHttpClient client, Consumer<String> onMessageReceived, Consumer<String> onComplete) {
super(client, onMessageReceived, onComplete);
public ChatCompletionClientEventListener(
OkHttpClient client,
Consumer<String> onMessageReceived,
Consumer<String> onComplete,
Consumer<String> onFailure) {
super(client, onMessageReceived, onComplete, onFailure);
}
protected String getMessage(String data) throws JsonProcessingException {

View file

@ -5,8 +5,11 @@ import static java.lang.String.format;
import ee.carlrobert.chatgpt.client.ApiRequestDetails;
import ee.carlrobert.chatgpt.client.BaseModel;
import ee.carlrobert.chatgpt.client.Client;
import ee.carlrobert.chatgpt.client.ClientCode;
import ee.carlrobert.chatgpt.ide.conversations.Conversation;
import ee.carlrobert.chatgpt.ide.conversations.message.Message;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import java.util.ArrayList;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@ -14,10 +17,10 @@ import okhttp3.sse.EventSourceListener;
public class TextCompletionClient extends Client {
private static final List<Map.Entry<String, String>> queries = new ArrayList<>();
private static TextCompletionClient instance;
private TextCompletionClient() {
super(ClientCode.TEXT_COMPLETIONS);
}
public static TextCompletionClient getInstance() {
@ -27,14 +30,10 @@ public class TextCompletionClient extends Client {
return instance;
}
public void clearPreviousSession() {
queries.clear();
}
protected ApiRequestDetails getRequestDetails(String prompt) {
return new ApiRequestDetails(
format("https://api.openai.com/v1/engines/%s/completions",
SettingsState.getInstance().textCompletionBaseModel.getModel()),
SettingsState.getInstance().textCompletionBaseModel.getCode()),
Map.of(
"stop", List.of(" Human:", " AI:"),
"prompt", buildPrompt(prompt),
@ -49,11 +48,18 @@ public class TextCompletionClient extends Client {
SettingsState.getInstance().apiKey);
}
protected EventSourceListener getEventSourceListener(Consumer<String> onMessageReceived, Runnable onComplete) {
protected EventSourceListener getEventSourceListener(
Consumer<String> onMessageReceived,
Consumer<Conversation> onComplete,
Consumer<String> onFailure) {
return new TextCompletionClientEventListener(client, onMessageReceived, (finalMessage) -> {
queries.add(Map.entry(prompt, finalMessage));
onComplete.run();
});
var message = new Message();
message.setPrompt(prompt);
message.setResponse(finalMessage);
conversation.setUpdatedOn(LocalDateTime.now());
conversation.addMessage(message);
onComplete.accept(conversation);
}, onFailure);
}
private StringBuilder getBasePrompt() {
@ -69,12 +75,12 @@ public class TextCompletionClient extends Client {
private String buildPrompt(String prompt) {
var basePrompt = getBasePrompt();
queries.forEach(query ->
conversation.getMessages().forEach(message ->
basePrompt.append("Human: ")
.append(query.getKey())
.append(message.getPrompt())
.append("\n")
.append("AI: ")
.append(query.getValue())
.append(message.getResponse())
.append("\n"));
basePrompt.append("Human: ")
.append(prompt)

View file

@ -9,8 +9,12 @@ import okhttp3.OkHttpClient;
public class TextCompletionClientEventListener extends CompletionClientEventListener {
public TextCompletionClientEventListener(OkHttpClient client, Consumer<String> onMessageReceived, Consumer<String> onComplete) {
super(client, onMessageReceived, onComplete);
public TextCompletionClientEventListener(
OkHttpClient client,
Consumer<String> onMessageReceived,
Consumer<String> onComplete,
Consumer<String> onFailure) {
super(client, onMessageReceived, onComplete, onFailure);
}
protected String getMessage(String data) throws JsonProcessingException {

View file

@ -2,8 +2,11 @@ package ee.carlrobert.chatgpt.client.unofficial;
import ee.carlrobert.chatgpt.client.ApiRequestDetails;
import ee.carlrobert.chatgpt.client.Client;
import ee.carlrobert.chatgpt.client.unofficial.response.ApiResponse;
import ee.carlrobert.chatgpt.client.ClientCode;
import ee.carlrobert.chatgpt.ide.conversations.Conversation;
import ee.carlrobert.chatgpt.ide.conversations.message.Message;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -14,9 +17,9 @@ import okhttp3.sse.EventSourceListener;
public class UnofficialChatGPTClient extends Client {
private static UnofficialChatGPTClient instance;
private static ApiResponse lastReceivedResponse;
private UnofficialChatGPTClient() {
super(ClientCode.UNOFFICIAL_CHATGPT);
}
public static UnofficialChatGPTClient getInstance() {
@ -26,17 +29,21 @@ public class UnofficialChatGPTClient extends Client {
return instance;
}
public void clearPreviousSession() {
lastReceivedResponse = null;
}
protected EventSourceListener getEventSourceListener(Consumer<String> onMessageReceived, Runnable onComplete) {
protected EventSourceListener getEventSourceListener(
Consumer<String> onMessageReceived,
Consumer<Conversation> onComplete,
Consumer<String> onFailure) {
return new UnofficialClientEventListener(client, prompt, onMessageReceived, (response) -> {
if (response != null) {
lastReceivedResponse = response;
}
onComplete.run();
});
var message = new Message();
message.setPrompt(prompt);
message.setResponse(response.getFullMessage());
conversation.setUnofficialClientConversationId(response.getConversationId());
conversation.setParentMessageId(response.getMessage().getId());
conversation.setUpdatedOn(LocalDateTime.now());
conversation.addMessage(message);
onComplete.accept(conversation);
}, onFailure);
}
protected ApiRequestDetails getRequestDetails(String prompt) {
@ -55,9 +62,12 @@ public class UnofficialChatGPTClient extends Client {
"model", "text-davinci-002-render-sha"
));
if (lastReceivedResponse != null) {
payload.put("conversation_id", lastReceivedResponse.getConversationId());
payload.put("parent_message_id", lastReceivedResponse.getMessage().getId());
var conversationId = conversation.getUnofficialClientConversationId();
var parentMessageId = conversation.getParentMessageId();
if (conversationId != null && !conversationId.isEmpty() &&
parentMessageId != null && !parentMessageId.isEmpty()) {
payload.put("conversation_id", conversationId);
payload.put("parent_message_id", parentMessageId);
} else {
payload.put("parent_message_id", UUID.randomUUID().toString());
}

View file

@ -23,16 +23,23 @@ public class UnofficialClientEventListener extends EventSourceListener {
private final ObjectMapper objectMapper = new ObjectMapper();
private final String prompt;
private final Consumer<String> onMessageReceived;
private final Consumer<ApiResponse> onComplete;
private final Consumer<@NotNull ApiResponse> onComplete;
private final Consumer<String> onFailure;
private ApiResponse lastReceivedResponse;
private boolean eventReceived = false;
private final OkHttpClient client;
public UnofficialClientEventListener(OkHttpClient client, String prompt, Consumer<String> onMessageReceived, Consumer<ApiResponse> onComplete) {
public UnofficialClientEventListener(
OkHttpClient client,
String prompt,
Consumer<String> onMessageReceived,
Consumer<ApiResponse> onComplete,
Consumer<String> onFailure) {
this.client = client;
this.prompt = prompt;
this.onMessageReceived = onMessageReceived;
this.onComplete = onComplete;
this.onFailure = onFailure;
}
public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) {
@ -45,11 +52,7 @@ public class UnofficialClientEventListener extends EventSourceListener {
var response = client.newCall(UnofficialChatGPTClient.getInstance().buildHttpRequest(prompt)).execute();
ResponseBody responseBody = response.body();
if (responseBody != null) {
var message = tryExtractingErrorMessage(responseBody.string());
message.ifPresent(msg -> {
onMessageReceived.accept(msg);
onComplete.accept(null);
});
tryExtractingErrorMessage(responseBody.string()).ifPresent(onFailure);
responseBody.close();
}
} catch (IOException e) {
@ -93,9 +96,8 @@ public class UnofficialClientEventListener extends EventSourceListener {
@Nullable Throwable ex,
@Nullable Response response) {
if (isRequestNotCancelled()) {
onMessageReceived.accept("Something went wrong. Please try again later. ");
onFailure.accept("Something went wrong. Please try again later.");
}
onComplete.accept(null);
}
private boolean isRequestNotCancelled() {

View file

@ -2,8 +2,10 @@ package ee.carlrobert.chatgpt.ide.action;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import ee.carlrobert.chatgpt.client.ClientFactory;
import ee.carlrobert.chatgpt.ide.conversations.ConversationsState;
import ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowService;
import java.util.Arrays;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
public class AskAction extends AnAction {
@ -19,9 +21,20 @@ public class AskAction extends AnAction {
if (project != null) {
var toolWindowService = project.getService(ToolWindowService.class);
var toolWindow = toolWindowService.getToolWindow(project);
new ClientFactory().getClient().clearPreviousSession();
var contentManager = toolWindow.getContentManager();
Arrays.stream(contentManager.getContents())
.filter(it -> "Chat".equals(it.getTabName()))
.findFirst()
.ifPresentOrElse(
contentManager::setSelectedContent,
() -> contentManager.setSelectedContent(Objects.requireNonNull(contentManager.getContent(0)))
);
ConversationsState.getInstance().startConversation();
toolWindow.show();
toolWindow.setTitle("");
toolWindow.setTitle("Chat");
toolWindowService.removeAll();
toolWindowService.paintLandingView();
}

View file

@ -5,15 +5,14 @@ import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import ee.carlrobert.chatgpt.client.ClientFactory;
import ee.carlrobert.chatgpt.ide.conversations.ConversationsState;
import ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowService;
import java.util.Arrays;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
public abstract class BaseAction extends AnAction {
protected abstract void initToolWindow(ToolWindow toolWindow);
protected abstract void actionPerformed(Project project, Editor editor, String selectedText);
public void actionPerformed(@NotNull AnActionEvent event) {
@ -36,8 +35,20 @@ public abstract class BaseAction extends AnAction {
protected void sendMessage(Project project, String prompt) {
var toolWindowService = project.getService(ToolWindowService.class);
new ClientFactory().getClient().clearPreviousSession();
initToolWindow(toolWindowService.getToolWindow(project));
var toolWindow = toolWindowService.getToolWindow(project);
var contentManager = toolWindow.getContentManager();
Arrays.stream(contentManager.getContents())
.filter(it -> "Chat".equals(it.getTabName()))
.findFirst()
.ifPresentOrElse(
contentManager::setSelectedContent,
() -> contentManager.setSelectedContent(Objects.requireNonNull(contentManager.getContent(0)))
);
ConversationsState.getInstance().startConversation();
toolWindowService.getToolWindow(project).show();
toolWindowService.removeAll();
toolWindowService.paintUserMessage(prompt);
toolWindowService.sendMessage(prompt, project, null);

View file

@ -3,7 +3,6 @@ package ee.carlrobert.chatgpt.ide.action;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.SwingUtilities;
@ -12,11 +11,6 @@ public class CustomPromptAction extends BaseAction {
private static String previousUserPrompt = "";
protected void initToolWindow(ToolWindow toolWindow) {
toolWindow.setTitle("Custom Prompt");
toolWindow.show();
}
protected void actionPerformed(Project project, Editor editor, String selectedText) {
if (selectedText != null && !selectedText.isEmpty()) {
var fileExtension = getFileExtension(((EditorImpl) editor).getVirtualFile().getName());

View file

@ -2,15 +2,9 @@ package ee.carlrobert.chatgpt.ide.action;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
public class ExplainAction extends BaseAction {
protected void initToolWindow(ToolWindow toolWindow) {
toolWindow.setTitle("Explain Code");
toolWindow.show();
}
protected void actionPerformed(Project project, Editor editor, String selectedText) {
sendMessage(project, "Explain the following code:\n\n" + selectedText);
}

View file

@ -2,15 +2,9 @@ package ee.carlrobert.chatgpt.ide.action;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
public class FindBugsAction extends BaseAction {
protected void initToolWindow(ToolWindow toolWindow) {
toolWindow.setTitle("Find Bugs");
toolWindow.show();
}
protected void actionPerformed(Project project, Editor editor, String selectedText) {
sendMessage(project, "Find bugs in the following code:\n\n" + selectedText);
}

View file

@ -2,15 +2,9 @@ package ee.carlrobert.chatgpt.ide.action;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
public class OptimizeAction extends BaseAction {
protected void initToolWindow(ToolWindow toolWindow) {
toolWindow.setTitle("Optimize Code");
toolWindow.show();
}
protected void actionPerformed(Project project, Editor editor, String selectedText) {
sendMessage(project, "Optimize the following code:\n\n" + selectedText);
}

View file

@ -2,15 +2,9 @@ package ee.carlrobert.chatgpt.ide.action;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
public class RefactorAction extends BaseAction {
protected void initToolWindow(ToolWindow toolWindow) {
toolWindow.setTitle("Refactor Code");
toolWindow.show();
}
protected void actionPerformed(Project project, Editor editor, String selectedText) {
sendMessage(project, "Refactor the following code:\n\n" + selectedText);
}

View file

@ -2,15 +2,9 @@ package ee.carlrobert.chatgpt.ide.action;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
public class WriteTestsAction extends BaseAction {
protected void initToolWindow(ToolWindow toolWindow) {
toolWindow.setTitle("Write Tests");
toolWindow.show();
}
protected void actionPerformed(Project project, Editor editor, String selectedText) {
sendMessage(project, "Generate unit tests for the following code:\n\n" + selectedText);
}

View file

@ -0,0 +1,89 @@
package ee.carlrobert.chatgpt.ide.conversations;
import ee.carlrobert.chatgpt.client.BaseModel;
import ee.carlrobert.chatgpt.client.ClientCode;
import ee.carlrobert.chatgpt.ide.conversations.message.Message;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class Conversation {
private UUID id;
private String parentMessageId;
private String unofficialClientConversationId;
private List<Message> messages = new ArrayList<>();
private ClientCode clientCode;
private BaseModel model;
private LocalDateTime createdOn;
private LocalDateTime updatedOn;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getParentMessageId() {
return parentMessageId;
}
public void setParentMessageId(String parentMessageId) {
this.parentMessageId = parentMessageId;
}
public String getUnofficialClientConversationId() {
return unofficialClientConversationId;
}
public void setUnofficialClientConversationId(String unofficialClientConversationId) {
this.unofficialClientConversationId = unofficialClientConversationId;
}
public List<Message> getMessages() {
return messages;
}
public void setMessages(List<Message> messages) {
this.messages = messages;
}
public ClientCode getClientCode() {
return clientCode;
}
public void setClientCode(ClientCode clientCode) {
this.clientCode = clientCode;
}
public void addMessage(Message message) {
messages.add(message);
}
public BaseModel getModel() {
return model;
}
public void setModel(BaseModel model) {
this.model = model;
}
public LocalDateTime getCreatedOn() {
return createdOn;
}
public void setCreatedOn(LocalDateTime createdOn) {
this.createdOn = createdOn;
}
public LocalDateTime getUpdatedOn() {
return updatedOn;
}
public void setUpdatedOn(LocalDateTime updatedOn) {
this.updatedOn = updatedOn;
}
}

View file

@ -0,0 +1,19 @@
package ee.carlrobert.chatgpt.ide.conversations;
import ee.carlrobert.chatgpt.client.ClientCode;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ConversationsContainer {
private Map<ClientCode, List<Conversation>> conversationsMapping = new HashMap<>();
public Map<ClientCode, List<Conversation>> getConversationsMapping() {
return conversationsMapping;
}
public void setConversationsMapping(Map<ClientCode, List<Conversation>> conversationsMapping) {
this.conversationsMapping = conversationsMapping;
}
}

View file

@ -0,0 +1,102 @@
package ee.carlrobert.chatgpt.ide.conversations;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.util.xmlb.XmlSerializerUtil;
import com.intellij.util.xmlb.annotations.OptionTag;
import ee.carlrobert.chatgpt.client.ClientCode;
import ee.carlrobert.chatgpt.client.ClientFactory;
import ee.carlrobert.chatgpt.ide.conversations.converter.ConversationConverter;
import ee.carlrobert.chatgpt.ide.conversations.converter.ConversationsConverter;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.UUID;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@State(
name = "ee.carlrobert.chatgpt.ide.conversations.ConversationsState",
storages = @Storage("ChatGPTConversations.xml")
)
public class ConversationsState implements PersistentStateComponent<ConversationsState> {
@OptionTag(converter = ConversationsConverter.class)
public ConversationsContainer conversationsContainer = new ConversationsContainer();
@OptionTag(converter = ConversationConverter.class)
public Conversation currentConversation;
public static ConversationsState getInstance() {
return ApplicationManager.getApplication().getService(ConversationsState.class);
}
@Nullable
@Override
public ConversationsState getState() {
return this;
}
@Override
public void loadState(@NotNull ConversationsState state) {
XmlSerializerUtil.copyBean(state, this);
}
public void setCurrentConversation(@Nullable Conversation conversation) {
this.currentConversation = conversation;
}
public static @Nullable Conversation getCurrentConversation() {
return getInstance().currentConversation;
}
public Conversation createConversation(ClientCode clientCode) {
var settings = SettingsState.getInstance();
var conversation = new Conversation();
conversation.setId(UUID.randomUUID());
conversation.setClientCode(clientCode);
if (!settings.isChatGPTOptionSelected) {
if (settings.isChatCompletionOptionSelected) {
conversation.setModel(settings.chatCompletionBaseModel);
} else {
conversation.setModel(settings.textCompletionBaseModel);
}
}
conversation.setCreatedOn(LocalDateTime.now());
conversation.setUpdatedOn(LocalDateTime.now());
return conversation;
}
public void addConversation(Conversation conversation) {
var conversationsMapping = conversationsContainer.getConversationsMapping();
var conversations = conversationsMapping.get(conversation.getClientCode());
if (conversations == null) {
conversations = new ArrayList<>();
}
conversations.add(conversation);
conversationsMapping.put(conversation.getClientCode(), conversations);
}
public void saveConversation(Conversation conversation) {
conversation.setUpdatedOn(LocalDateTime.now());
var iterator = conversationsContainer.getConversationsMapping()
.get(conversation.getClientCode())
.listIterator();
while (iterator.hasNext()) {
var next = iterator.next();
if (next.getId().equals(conversation.getId())) {
iterator.set(conversation);
}
}
setCurrentConversation(conversation);
}
public Conversation startConversation() {
var conversation = createConversation(new ClientFactory().getClient().getCode());
setCurrentConversation(conversation);
addConversation(conversation);
return conversation;
}
}

View file

@ -0,0 +1,37 @@
package ee.carlrobert.chatgpt.ide.conversations.converter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.intellij.util.xmlb.Converter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
abstract class BaseConverter<T> extends Converter<T> {
private final Class<T> clazz;
private final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module())
.registerModule(new JavaTimeModule());
BaseConverter(Class<T> clazz) {
this.clazz = clazz;
}
public @Nullable T fromString(@NotNull String value) {
try {
return objectMapper.readValue(value, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException("Unable to deserialize conversations", e);
}
}
public @Nullable String toString(@NotNull T value) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new RuntimeException("Unable to serialize conversations", e);
}
}
}

View file

@ -0,0 +1,10 @@
package ee.carlrobert.chatgpt.ide.conversations.converter;
import ee.carlrobert.chatgpt.ide.conversations.Conversation;
public class ConversationConverter extends BaseConverter<Conversation> {
public ConversationConverter() {
super(Conversation.class);
}
}

View file

@ -0,0 +1,10 @@
package ee.carlrobert.chatgpt.ide.conversations.converter;
import ee.carlrobert.chatgpt.ide.conversations.ConversationsContainer;
public class ConversationsConverter extends BaseConverter<ConversationsContainer> {
public ConversationsConverter() {
super(ConversationsContainer.class);
}
}

View file

@ -0,0 +1,23 @@
package ee.carlrobert.chatgpt.ide.conversations.message;
public class Message {
private String prompt;
private String response;
public String getPrompt() {
return prompt;
}
public void setPrompt(String prompt) {
this.prompt = prompt;
}
public String getResponse() {
return response;
}
public void setResponse(String response) {
this.response = response;
}
}

View file

@ -1,6 +1,7 @@
package ee.carlrobert.chatgpt.ide.settings;
import com.intellij.openapi.options.Configurable;
import ee.carlrobert.chatgpt.ide.conversations.ConversationsState;
import javax.swing.JComponent;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.Nullable;
@ -36,18 +37,20 @@ public class SettingsConfigurable implements Configurable {
!settingsComponent.getProxyHost().equals(settings.proxyHost) ||
settingsComponent.getProxyPort() != settings.proxyPort ||
!settingsComponent.getProxyType().equals(settings.proxyType) ||
!settingsComponent.getReverseProxyUrl().equals(settings.reverseProxyUrl) ||
!settingsComponent.getChatCompletionBaseModel().equals(settings.chatCompletionBaseModel) ||
!settingsComponent.getTextCompletionBaseModel().equals(settings.textCompletionBaseModel) ||
settingsComponent.isGPTOptionSelected() != settings.isGPTOptionSelected ||
settingsComponent.isChatCompletionOptionSelected() != settings.isChatCompletionOptionSelected ||
settingsComponent.isTextCompletionOptionSelected() != settings.isTextCompletionOptionSelected ||
settingsComponent.isChatGPTOptionSelected() != settings.isChatGPTOptionSelected;
!settingsComponent.getReverseProxyUrl().equals(settings.reverseProxyUrl) ||
isClientChanged(settings);
}
@Override
public void apply() {
var settings = SettingsState.getInstance();
if (isClientChanged(settings)) {
ConversationsState.getInstance().setCurrentConversation(null);
}
settings.isGPTOptionSelected = settingsComponent.isGPTOptionSelected();
settings.isChatGPTOptionSelected = settingsComponent.isChatGPTOptionSelected();
settings.accessToken = settingsComponent.getAccessToken();
@ -83,4 +86,11 @@ public class SettingsConfigurable implements Configurable {
public void disposeUIResources() {
settingsComponent = null;
}
private boolean isClientChanged(SettingsState settings) {
return settingsComponent.isGPTOptionSelected() != settings.isGPTOptionSelected ||
settingsComponent.isChatCompletionOptionSelected() != settings.isChatCompletionOptionSelected ||
settingsComponent.isTextCompletionOptionSelected() != settings.isTextCompletionOptionSelected ||
settingsComponent.isChatGPTOptionSelected() != settings.isChatGPTOptionSelected;
}
}

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="ee.carlrobert.chatgpt.ide.toolwindow.ChatGptToolWindow">
<grid id="27dc6" binding="chatGptToolWindowContent" layout-manager="GridLayoutManager" row-count="3" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="8" bottom="8" right="8"/>
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="530" height="400"/>
</constraints>

View file

@ -2,7 +2,9 @@ package ee.carlrobert.chatgpt.ide.toolwindow;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.ui.JBUI;
import ee.carlrobert.chatgpt.ide.toolwindow.components.GenerateButton;
import ee.carlrobert.chatgpt.ide.toolwindow.components.TextArea;
import java.awt.Adjustable;
@ -78,7 +80,7 @@ public class ChatGptToolWindow {
textAreaScrollPane.setBorder(null);
textAreaScrollPane.setViewportBorder(null);
textAreaScrollPane.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(),
BorderFactory.createMatteBorder(1, 0, 0, 0, JBColor.border()),
BorderFactory.createEmptyBorder(0, 5, 0, 10)));
textAreaScrollPane.setViewportView(textArea);
@ -90,13 +92,14 @@ public class ChatGptToolWindow {
scrollPane = new JBScrollPane();
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setViewportView(scrollablePanel);
scrollPane.setBorder(null);
scrollPane.setBorder(JBUI.Borders.empty(0, 8));
scrollPane.setViewportBorder(null);
generateButton = new GenerateButton();
var toolWindowService = project.getService(ToolWindowService.class);
toolWindowService.setGenerateButton((GenerateButton) generateButton); // TODO: Remove casting
toolWindowService.setScrollPane(scrollPane);
toolWindowService.setScrollablePanel(scrollablePanel);
toolWindowService.paintLandingView();
}

View file

@ -6,14 +6,49 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.ui.content.ContentFactory;
import com.intellij.ui.content.ContentManagerEvent;
import com.intellij.ui.content.ContentManagerListener;
import ee.carlrobert.chatgpt.ide.conversations.ConversationsState;
import ee.carlrobert.chatgpt.ide.toolwindow.conversations.ConversationsToolWindow;
import java.util.Arrays;
import javax.swing.JPanel;
import org.jetbrains.annotations.NotNull;
public class ChatGptToolWindowFactory implements ToolWindowFactory, DumbAware {
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
var content = ApplicationManager.getApplication()
addContent(toolWindow, new ChatGptToolWindow(project).getContent(), "Chat");
var conversationToolWidow = new ConversationsToolWindow(project, toolWindow);
addContent(toolWindow, conversationToolWidow.getContent(), "Conversation History");
toolWindow.addContentManagerListener(new ContentManagerListener() {
public void selectionChanged(@NotNull ContentManagerEvent event) {
if ("Conversation History".equals(event.getContent().getTabName()) && event.getContent().isSelected()) {
conversationToolWidow.refresh();
}
}
});
var conversation = ConversationsState.getCurrentConversation();
if (conversation != null) {
var toolWindowService = project.getService(ToolWindowService.class);
var contentManager = toolWindow.getContentManager();
Arrays.stream(contentManager.getContents())
.filter(content -> "Chat".equals(content.getTabName()))
.findFirst()
.ifPresent(
content -> {
if (contentManager.isSelected(content)) {
toolWindowService.displayConversation(conversation);
}
}
);
}
}
private void addContent(@NotNull ToolWindow toolWindow, JPanel content, String displayName) {
toolWindow.getContentManager().addContent(ApplicationManager.getApplication()
.getService(ContentFactory.class)
.createContent(new ChatGptToolWindow(project).getContent(), "", false);
toolWindow.getContentManager().addContent(content);
.createContent(content, displayName, false));
}
}

View file

@ -5,6 +5,7 @@ import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.createTextArea
import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.justifyLeft;
import static java.lang.String.format;
import com.intellij.icons.AllIcons;
import com.intellij.ide.ui.LafManager;
import com.intellij.ide.ui.LafManagerListener;
import com.intellij.openapi.options.ShowSettingsUtil;
@ -13,14 +14,20 @@ import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowManager;
import ee.carlrobert.chatgpt.client.ClientFactory;
import ee.carlrobert.chatgpt.ide.conversations.Conversation;
import ee.carlrobert.chatgpt.ide.conversations.ConversationsState;
import ee.carlrobert.chatgpt.ide.conversations.message.Message;
import ee.carlrobert.chatgpt.ide.settings.SettingsConfigurable;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import ee.carlrobert.chatgpt.ide.toolwindow.components.GenerateButton;
import ee.carlrobert.chatgpt.ide.toolwindow.components.GenerateButton.Mode;
import ee.carlrobert.chatgpt.ide.toolwindow.components.SyntaxTextArea;
import icons.Icons;
import java.awt.Adjustable;
import java.awt.Cursor;
import java.awt.GridBagLayout;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.lang.reflect.InvocationTargetException;
@ -28,9 +35,11 @@ import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
@ -41,28 +50,40 @@ public class ToolWindowService implements LafManagerListener {
private static final List<SyntaxTextArea> textAreas = new ArrayList<>();
private boolean isLandingViewVisible;
private ScrollablePanel scrollablePanel;
private JScrollPane scrollPane;
private GenerateButton generateButton;
public void setScrollablePanel(ScrollablePanel scrollablePanel) {
this.scrollablePanel = scrollablePanel;
}
public void setScrollPane(JScrollPane scrollPane) {
this.scrollPane = scrollPane;
}
public void setGenerateButton(GenerateButton generateButton) {
this.generateButton = generateButton;
}
@Override
public void lookAndFeelChanged(@NotNull LafManager source) {
for (var textArea : textAreas) {
textArea.changeStyleViaThemeXml();
}
}
public ToolWindow getToolWindow(@NotNull Project project) {
return ToolWindowManager.getInstance(project).getToolWindow("CodeGPT");
}
public void paintUserMessage(String userMessage) {
if (isLandingViewVisible) {
if (isLandingViewVisible || ConversationsState.getCurrentConversation() == null) {
removeAll();
}
addSpacing(8);
addIconLabel(Icons.UserImageIcon, "User:");
addIconLabel(AllIcons.General.User, "User:");
addSpacing(8);
scrollablePanel.add(createTextArea(userMessage, true));
scrollablePanel.add(createTextArea(userMessage));
}
public void sendMessage(String prompt, Project project, @Nullable Runnable scrollToBottom) {
@ -76,6 +97,12 @@ public class ToolWindowService implements LafManagerListener {
} else if (settings.isChatGPTOptionSelected && settings.accessToken.isEmpty()) {
notifyMissingCredential(project, "Access token not provided.");
} else {
var conversationsState = ConversationsState.getInstance();
var conversation = ConversationsState.getCurrentConversation();
if (conversation == null) {
conversation = conversationsState.startConversation();
}
var textArea = new SyntaxTextArea(true, true, SyntaxConstants.SYNTAX_STYLE_MARKDOWN);
scrollablePanel.add(textArea);
textAreas.add(textArea);
@ -83,35 +110,64 @@ public class ToolWindowService implements LafManagerListener {
var client = new ClientFactory().getClient();
generateButton.setVisible(true);
generateButton.setMode(Mode.STOP, client::cancelRequest);
client.getCompletionsAsync(prompt, message -> {
try {
SwingUtilities.invokeAndWait(
() -> {
textArea.append(message);
if (scrollToBottom != null) {
scrollToBottom.run();
}
}
);
} catch (InterruptedException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}, () -> {
generateButton.setMode(Mode.REFRESH, () -> {
sendMessage(prompt, project, scrollToBottom);
if (scrollToBottom != null) {
scrollToBottom.run();
}
});
textArea.displayCopyButton();
textArea.hideCaret();
if (scrollToBottom != null) {
scrollToBottom.run();
}
});
var conversationMessage = new Message();
conversationMessage.setPrompt(prompt);
client.getCompletionsAsync(
conversation,
prompt,
message -> {
try {
SwingUtilities.invokeAndWait(
() -> {
textArea.append(message);
// TODO: Should we set the text everytime?
conversationMessage.setResponse(textArea.getText());
if (scrollToBottom != null) {
scrollToBottom.run();
}
}
);
} catch (InterruptedException | InvocationTargetException e) {
textArea.append("Something went wrong. Please try again later.");
throw new RuntimeException(e);
}
},
(completedConversation) -> {
ConversationsState.getInstance().saveConversation(completedConversation);
stopGenerating(prompt, textArea, project, scrollToBottom);
},
(errorMessage) -> {
var currentConversation = ConversationsState.getCurrentConversation();
conversationMessage.setResponse(errorMessage);
currentConversation.addMessage(conversationMessage);
ConversationsState.getInstance().saveConversation(currentConversation);
textArea.append(errorMessage);
stopGenerating(prompt, textArea, project, scrollToBottom);
});
}
}
public void displayConversation(Conversation conversation) {
removeAll();
conversation.getMessages().forEach(message -> {
paintUserMessage(message.getPrompt());
addSpacing(8);
addIconLabel(Icons.DefaultImageIcon, "ChatGPT:");
addSpacing(8);
var textArea = new SyntaxTextArea(true, true, SyntaxConstants.SYNTAX_STYLE_MARKDOWN);
textArea.setText(message.getResponse());
textArea.displayCopyButton();
textArea.hideCaret();
scrollablePanel.add(textArea);
textAreas.add(textArea);
});
scrollToBottom();
}
public void paintLandingView() {
isLandingViewVisible = true;
@ -145,12 +201,26 @@ public class ToolWindowService implements LafManagerListener {
scrollablePanel.removeAll();
}
private void stopGenerating(String prompt, SyntaxTextArea textArea, Project project, @Nullable Runnable scrollToBottom) {
generateButton.setMode(Mode.REFRESH, () -> {
sendMessage(prompt, project, scrollToBottom);
if (scrollToBottom != null) {
scrollToBottom.run();
}
});
textArea.displayCopyButton();
textArea.hideCaret();
if (scrollToBottom != null) {
scrollToBottom.run();
}
}
private void addSpacing(int height) {
scrollablePanel.add(Box.createVerticalStrut(height));
}
private void addIconLabel(ImageIcon imageIcon, String text) {
scrollablePanel.add(justifyLeft(createIconLabel(imageIcon, text)));
private void addIconLabel(Icon icon, String text) {
scrollablePanel.add(justifyLeft(createIconLabel(icon, text)));
}
private void notifyMissingCredential(Project project, String text) {
@ -164,10 +234,14 @@ public class ToolWindowService implements LafManagerListener {
scrollablePanel.add(justifyLeft(label));
}
@Override
public void lookAndFeelChanged(@NotNull LafManager source) {
for (var textArea : textAreas) {
textArea.changeStyleViaThemeXml();
}
private void scrollToBottom() {
JScrollBar verticalBar = scrollPane.getVerticalScrollBar();
verticalBar.addAdjustmentListener(new AdjustmentListener() {
public void adjustmentValueChanged(AdjustmentEvent e) {
Adjustable adjustable = e.getAdjustable();
adjustable.setValue(adjustable.getMaximum());
verticalBar.removeAdjustmentListener(this);
}
});
}
}

View file

@ -8,7 +8,7 @@ import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JTextArea;
@ -16,32 +16,30 @@ import javax.swing.KeyStroke;
public class ToolWindowUtil {
public static JTextArea createTextArea(String selectedText, boolean isItalicFont) {
public static JTextArea createTextArea(String selectedText) {
var textArea = new JTextArea();
textArea.append(selectedText);
textArea.setLineWrap(true);
textArea.setEditable(false);
var font = textArea.getFont();
textArea.setFont(font.deriveFont(isItalicFont ? Font.ITALIC : Font.PLAIN));
textArea.setFont(textArea.getFont().deriveFont(Font.ITALIC));
textArea.setWrapStyleWord(true);
textArea.setBackground(JBColor.PanelBackground);
// textArea.setBorder(new MatteBorder(0, 2, 0, 0, JBColor.RED));
return textArea;
}
public static JLabel createIconLabel(ImageIcon imageIcon, String text) {
var iconLabel = new JLabel(imageIcon);
public static JLabel createIconLabel(Icon icon, String text) {
var iconLabel = new JLabel(icon);
iconLabel.setText(text);
iconLabel.setFont(iconLabel.getFont().deriveFont(iconLabel.getFont().getStyle() | Font.BOLD));
iconLabel.setIconTextGap(8);
return iconLabel;
}
public static JButton createIconButton(ImageIcon imageIcon) {
var button = new JButton(imageIcon);
public static JButton createIconButton(Icon icon) {
var button = new JButton(icon);
button.setBorder(BorderFactory.createEmptyBorder());
button.setContentAreaFilled(false);
button.setPreferredSize(new Dimension(imageIcon.getIconWidth(), imageIcon.getIconHeight()));
button.setPreferredSize(new Dimension(icon.getIconWidth(), icon.getIconHeight()));
return button;
}

View file

@ -1,6 +1,6 @@
package ee.carlrobert.chatgpt.ide.toolwindow.components;
import icons.Icons;
import com.intellij.icons.AllIcons;
import javax.swing.JButton;
import javax.swing.SwingConstants;
@ -12,7 +12,7 @@ public class GenerateButton extends JButton {
public void setMode(Mode mode, Runnable onClick) {
var isStopMode = mode == Mode.STOP;
setIcon(isStopMode ? Icons.SquareIcon : Icons.RefreshIcon);
setIcon(isStopMode ? AllIcons.Actions.Suspend : AllIcons.Actions.Refresh);
setText(isStopMode ? "Stop generating" : "Regenerate response");
for (var listener : getActionListeners()) {
removeActionListener(listener);

View file

@ -2,9 +2,9 @@ package ee.carlrobert.chatgpt.ide.toolwindow.components;
import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.createIconButton;
import com.intellij.icons.AllIcons;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import icons.Icons;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
@ -65,10 +65,10 @@ public class SyntaxTextArea extends RSyntaxTextArea {
}
private JButton createCopyButton() {
var button = createIconButton(Icons.CopyImageIcon);
var button = createIconButton(AllIcons.General.InlineCopy);
button.addActionListener(e -> {
copyToClipboard();
button.setIcon(Icons.DoubleTickImageIcon);
button.setIcon(AllIcons.General.InspectionsOK);
});
return button;
}

View file

@ -4,6 +4,7 @@ import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.addShiftEnterI
import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.createIconButton;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBTextArea;
import com.intellij.util.ui.JBUI;
import icons.Icons;
import java.awt.event.ActionListener;
@ -11,9 +12,8 @@ import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import javax.swing.JButton;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
public class TextArea extends JTextArea {
public class TextArea extends JBTextArea {
public TextArea(Runnable onSubmit, JScrollPane textAreaScrollPane) {
super("Ask me anything...");

View file

@ -0,0 +1,93 @@
package ee.carlrobert.chatgpt.ide.toolwindow.conversations;
import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.justifyLeft;
import com.intellij.icons.AllIcons;
import com.intellij.ui.JBColor;
import com.intellij.util.ui.JBUI;
import ee.carlrobert.chatgpt.ide.conversations.Conversation;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.time.format.DateTimeFormatter;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JPanel;
class ConversationPanel extends JPanel {
private final Conversation conversation;
ConversationPanel(Conversation conversation, boolean isSelected) {
this.conversation = conversation;
addStyles(isSelected);
var constraints = new GridBagConstraints();
constraints.insets = JBUI.insets(0, 10);
addChatIcon(constraints);
addTextPanel(constraints);
}
private void addStyles(boolean isSelected) {
setBackground(JBColor.background().darker());
if (isSelected) {
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(4, 4, 4, 4, JBColor.green),
JBUI.Borders.empty(10)));
} else {
setBorder(JBUI.Borders.empty(10));
}
setLayout(new GridBagLayout());
setCursor(new Cursor(Cursor.HAND_CURSOR));
}
private void addChatIcon(GridBagConstraints constraints) {
constraints.gridx = 0;
constraints.gridy = 0;
constraints.weightx = 0.0;
constraints.fill = GridBagConstraints.NONE;
add(new JLabel(AllIcons.Actions.Annotate), constraints);
}
private void addTextPanel(GridBagConstraints constraints) {
constraints.gridx = 1;
constraints.weightx = 1.0;
constraints.fill = GridBagConstraints.HORIZONTAL;
add(createTextPanel(), constraints);
}
private JPanel createTextPanel() {
var title = new JLabel(getFirstPrompt());
title.setBorder(JBUI.Borders.emptyBottom(8));
title.setFont(title.getFont().deriveFont(title.getFont().getStyle() | Font.BOLD));
var textPanel = new JPanel();
textPanel.setBackground(getBackground());
textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.PAGE_AXIS));
textPanel.add(justifyLeft(title));
var bottomPanel = new JPanel();
bottomPanel.setBackground(getBackground());
bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.X_AXIS));
bottomPanel.add(new JLabel(conversation.getUpdatedOn()
.format(DateTimeFormatter.ofPattern("M/d/yyyy, h:mm:ss a"))));
bottomPanel.add(Box.createHorizontalGlue());
if (conversation.getModel() != null) {
bottomPanel.add(new JLabel(conversation.getModel().getCode()));
}
textPanel.add(bottomPanel);
return textPanel;
}
private String getFirstPrompt() {
var messages = conversation.getMessages();
var prompt = "";
if (!messages.isEmpty()) {
prompt = conversation.getMessages().get(0).getPrompt();
}
return prompt;
}
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="ee.carlrobert.chatgpt.ide.toolwindow.conversations.ConversationsToolWindow">
<grid id="27dc6" binding="conversationsToolWindowContent" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<scrollpane id="77046" binding="scrollPane" custom-create="true">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="7" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</scrollpane>
</children>
</grid>
</form>

View file

@ -0,0 +1,80 @@
package ee.carlrobert.chatgpt.ide.toolwindow.conversations;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.ui.components.JBScrollPane;
import ee.carlrobert.chatgpt.ide.conversations.Conversation;
import ee.carlrobert.chatgpt.ide.conversations.ConversationsState;
import ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowService;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import javax.swing.BoxLayout;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import org.jetbrains.annotations.NotNull;
public class ConversationsToolWindow {
private final Project project;
private final ToolWindow toolWindow;
private JPanel conversationsToolWindowContent;
private JScrollPane scrollPane;
private ScrollablePanel scrollablePanel;
public ConversationsToolWindow(@NotNull Project project, @NotNull ToolWindow toolWindow) {
this.project = project;
this.toolWindow = toolWindow;
refresh();
}
public JPanel getContent() {
return conversationsToolWindowContent;
}
public void refresh() {
scrollablePanel.removeAll();
ConversationsState.getInstance()
.conversationsContainer
.getConversationsMapping()
.forEach((key, value) -> value.stream()
.sorted(Comparator.comparing(Conversation::getUpdatedOn).reversed())
.forEach(this::addContent));
}
private void addContent(Conversation conversation) {
var mainPanel = new RootConversationPanel(() -> {
ConversationsState.getInstance().setCurrentConversation(conversation);
var toolWindowService = project.getService(ToolWindowService.class);
var contentManager = toolWindow.getContentManager();
Arrays.stream(contentManager.getContents())
.filter(content -> "Chat".equals(content.getTabName()))
.findFirst()
.ifPresentOrElse(
contentManager::setSelectedContent,
() -> contentManager.setSelectedContent(Objects.requireNonNull(contentManager.getContent(0)))
);
toolWindowService.displayConversation(conversation);
});
var currentConversation = ConversationsState.getCurrentConversation();
var isSelected = currentConversation != null && currentConversation.getId().equals(conversation.getId());
mainPanel.setBackground(conversationsToolWindowContent.getBackground());
mainPanel.add(new ConversationPanel(conversation, isSelected));
scrollablePanel.add(mainPanel);
}
private void createUIComponents() {
scrollablePanel = new ScrollablePanel();
scrollablePanel.setLayout(new BoxLayout(scrollablePanel, BoxLayout.Y_AXIS));
scrollPane = new JBScrollPane();
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setViewportView(scrollablePanel);
scrollPane.setBorder(null);
scrollPane.setViewportBorder(null);
}
}

View file

@ -0,0 +1,43 @@
package ee.carlrobert.chatgpt.ide.toolwindow.conversations;
import com.intellij.ui.JBColor;
import com.intellij.util.ui.JBUI;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.BoxLayout;
import javax.swing.JPanel;
class RootConversationPanel extends JPanel {
RootConversationPanel(Runnable onClick) {
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setBorder(JBUI.Borders.empty(10, 20));
setBackground(JBColor.background());
addMouseListener(getMouseListener(onClick));
}
private MouseListener getMouseListener(Runnable onClick) {
return new MouseListener() {
@Override
public void mouseClicked(MouseEvent mouseEvent) {
onClick.run();
}
@Override
public void mousePressed(MouseEvent mouseEvent) {
}
@Override
public void mouseReleased(MouseEvent mouseEvent) {
}
@Override
public void mouseEntered(MouseEvent mouseEvent) {
}
@Override
public void mouseExited(MouseEvent mouseEvent) {
}
};
}
}

View file

@ -7,17 +7,11 @@ import javax.swing.ImageIcon;
public class Icons {
public static final Icon DefaultIcon = IconLoader.getIcon("/icons/chatgpt-icon.svg", Icons.class);
public static final Icon SquareIcon = IconLoader.getIcon("/icons/square-icon.svg", Icons.class);
public static final Icon RefreshIcon = IconLoader.getIcon("/icons/refresh-icon.svg", Icons.class);
public static final Icon CommandIcon = IconLoader.getIcon("/icons/command-icon.svg", Icons.class);
public static final Icon QuestionMarkIcon = IconLoader.getIcon("/icons/question-mark-icon.svg", Icons.class);
public static final Icon DefaultIcon = IconLoader.getIcon("/icons/codegpt-icon.svg", Icons.class);
public static final Icon ToolWindowIcon = IconLoader.getIcon("/icons/toolwindow-icon.svg", Icons.class);
public static final ImageIcon DefaultImageIcon = getImageIcon("/icons/chatgpt-icon.png");
public static final ImageIcon SendImageIcon = getImageIcon("/icons/send-icon.png");
public static final ImageIcon SunImageIcon = getImageIcon("/icons/sun-icon.png");
public static final ImageIcon UserImageIcon = getImageIcon("/icons/user-icon.png");
public static final ImageIcon CopyImageIcon = getImageIcon("/icons/copy-icon.png");
public static final ImageIcon DoubleTickImageIcon = getImageIcon("/icons/double-tick-icon.png");
private static ImageIcon getImageIcon(String path) {
return new ImageIcon(Objects.requireNonNull(Icons.class.getResource(path)));