feat: OpenAI and Claude vision support (#430)

* feat: add OpenAI and Claude vision support

* refactor: replace awaitility with PlatformTestUtil.waitWithEventsDispatching

* feat: display error when image not found

* chore: bump llm-client

* feat: configurable file watcher and minor code cleanup

* fix: ensure image notifications are triggered only for image file types

* docs: update changelog

* fix: user textarea icon button behaviour

* refactor: minor cleanup
This commit is contained in:
Carl-Robert 2024-04-02 02:50:41 +03:00 committed by GitHub
parent 99b5f94dad
commit 8cf5720db9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 793 additions and 309 deletions

View file

@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Vision support (image understanding) for OpenAI GPT-4 and Anthropic Claude models
### Removed
- Azure custom configuration (use OpenAI-compatible service to override the default configuration)

View file

@ -59,8 +59,6 @@ dependencies {
implementation("org.jsoup:jsoup:1.17.2")
implementation("org.apache.commons:commons-text:1.11.0")
implementation("com.knuddels:jtokkit:1.0.0")
testImplementation("org.awaitility:awaitility:4.2.0")
}
tasks.register<Exec>("updateSubmodules") {

View file

@ -26,7 +26,7 @@ checkstyle {
}
dependencies {
implementation("ee.carlrobert:llm-client:0.6.2")
implementation("ee.carlrobert:llm-client:0.7.0")
testImplementation("org.assertj:assertj-core:3.25.3")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.2")

View file

@ -9,4 +9,6 @@ public class CodeGPTKeys {
Key.create("codegpt.editor.inlay.prev-value");
public static final Key<List<ReferencedFile>> SELECTED_FILES =
Key.create("codegpt.selectedFiles");
public static final Key<String> IMAGE_ATTACHMENT_FILE_PATH =
Key.create("codegpt.imageAttachmentFilePath");
}

View file

@ -9,7 +9,10 @@ import com.knuddels.jtokkit.api.EncodingRegistry;
import com.knuddels.jtokkit.api.EncodingType;
import com.knuddels.jtokkit.api.IntArrayList;
import ee.carlrobert.codegpt.conversations.Conversation;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionDetailedMessage;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIMessageTextContent;
import java.util.List;
@Service
@ -38,7 +41,16 @@ public final class EncodingManager {
}
public int countMessageTokens(OpenAIChatCompletionMessage message) {
return countMessageTokens(message.getRole(), message.getContent());
if (message instanceof OpenAIChatCompletionStandardMessage standardMessage) {
return countMessageTokens(standardMessage.getRole(), standardMessage.getContent());
}
return ((OpenAIChatCompletionDetailedMessage) message).getContent().stream()
.filter(it -> it instanceof OpenAIMessageTextContent)
.map(it -> countMessageTokens(
((OpenAIChatCompletionDetailedMessage) message).getRole(),
((OpenAIMessageTextContent) it).getText()))
.reduce(0, Integer::sum);
}
public int countMessageTokens(String role, String content) {

View file

@ -17,4 +17,5 @@ public final class Icons {
public static final Icon You = IconLoader.getIcon("/icons/you.svg", Icons.class);
public static final Icon YouSmall = IconLoader.getIcon("/icons/you_small.png", Icons.class);
public static final Icon User = IconLoader.getIcon("/icons/user.svg", Icons.class);
public static final Icon Upload = IconLoader.getIcon("/icons/upload.svg", Icons.class);
}

View file

@ -53,7 +53,7 @@ public class ProjectCompilationStatusListener implements CompilationStatusListen
() -> project.getService(StandardChatToolWindowContentManager.class)
.sendMessage(getMultiFileMessage(compileContext), FIX_COMPILE_ERRORS)))
.addAction(NotificationAction.createSimpleExpiring(
CodeGPTBundle.get("checkForUpdatesTask.notification.hideButton"),
CodeGPTBundle.get("shared.notification.doNotShowAgain"),
() -> ConfigurationSettings.getCurrentState().setCaptureCompileErrors(false)))
.notify(project);
}

View file

@ -2,6 +2,7 @@ package ee.carlrobert.codegpt.completions;
import ee.carlrobert.codegpt.conversations.Conversation;
import ee.carlrobert.codegpt.conversations.message.Message;
import org.jetbrains.annotations.Nullable;
public class CallParameters {
@ -9,6 +10,8 @@ public class CallParameters {
private final ConversationType conversationType;
private final Message message;
private final boolean retry;
private @Nullable String imageMediaType;
private byte[] imageData;
public CallParameters(Conversation conversation, Message message) {
this(conversation, ConversationType.DEFAULT, message, false);
@ -40,4 +43,20 @@ public class CallParameters {
public boolean isRetry() {
return retry;
}
public String getImageMediaType() {
return imageMediaType;
}
public void setImageMediaType(String imageMediaType) {
this.imageMediaType = imageMediaType;
}
public byte[] getImageData() {
return imageData;
}
public void setImageData(byte[] imageData) {
this.imageData = imageData;
}
}

View file

@ -30,15 +30,29 @@ import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings;
import ee.carlrobert.codegpt.settings.service.you.YouSettings;
import ee.carlrobert.codegpt.telemetry.core.configuration.TelemetryConfiguration;
import ee.carlrobert.codegpt.telemetry.core.service.UserId;
import ee.carlrobert.codegpt.util.file.FileUtil;
import ee.carlrobert.llm.client.anthropic.completion.ClaudeBase64Source;
import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionDetailedMessage;
import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionMessage;
import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequest;
import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequestMessage;
import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionStandardMessage;
import ee.carlrobert.llm.client.anthropic.completion.ClaudeMessageImageContent;
import ee.carlrobert.llm.client.anthropic.completion.ClaudeMessageTextContent;
import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest;
import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionDetailedMessage;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIImageUrl;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIMessageImageURLContent;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIMessageTextContent;
import ee.carlrobert.llm.client.you.completion.YouCompletionRequest;
import ee.carlrobert.llm.client.you.completion.YouCompletionRequestMessage;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -91,9 +105,10 @@ public class CompletionRequestProvider {
public static OpenAIChatCompletionRequest buildOpenAILookupCompletionRequest(String context) {
return new OpenAIChatCompletionRequest.Builder(
List.of(
new OpenAIChatCompletionMessage("system",
new OpenAIChatCompletionStandardMessage(
"system",
getResourceContent("/prompts/method-name-generator.txt")),
new OpenAIChatCompletionMessage("user", context)))
new OpenAIChatCompletionStandardMessage("user", context)))
.setModel(OpenAISettings.getCurrentState().getModel())
.setStream(false)
.build();
@ -103,8 +118,8 @@ public class CompletionRequestProvider {
return buildCustomOpenAIChatCompletionRequest(
CustomServiceSettings.getCurrentState(),
List.of(
new OpenAIChatCompletionMessage("system", system),
new OpenAIChatCompletionMessage("user", context)),
new OpenAIChatCompletionStandardMessage("system", system),
new OpenAIChatCompletionStandardMessage("user", context)),
true);
}
@ -112,10 +127,10 @@ public class CompletionRequestProvider {
return buildCustomOpenAIChatCompletionRequest(
CustomServiceSettings.getCurrentState(),
List.of(
new OpenAIChatCompletionMessage(
new OpenAIChatCompletionStandardMessage(
"system",
getResourceContent("/prompts/method-name-generator.txt")),
new OpenAIChatCompletionMessage("user", context)),
new OpenAIChatCompletionStandardMessage("user", context)),
false);
}
@ -246,15 +261,25 @@ public class CompletionRequestProvider {
request.setMaxTokens(configuration.getMaxTokens());
request.setStream(true);
request.setSystem(COMPLETION_SYSTEM_PROMPT);
var messages = conversation.getMessages().stream()
List<ClaudeCompletionMessage> messages = conversation.getMessages().stream()
.filter(prevMessage -> prevMessage.getResponse() != null
&& !prevMessage.getResponse().isEmpty())
.flatMap(prevMessage -> Stream.of(
new ClaudeCompletionRequestMessage("user", prevMessage.getPrompt()),
new ClaudeCompletionRequestMessage("assistant", prevMessage.getResponse())))
new ClaudeCompletionStandardMessage("user", prevMessage.getPrompt()),
new ClaudeCompletionStandardMessage("assistant", prevMessage.getResponse())))
.collect(toList());
messages.add(
new ClaudeCompletionRequestMessage("user", callParameters.getMessage().getPrompt()));
if (callParameters.getImageMediaType() != null && callParameters.getImageData().length > 0) {
messages.add(new ClaudeCompletionDetailedMessage("user",
List.of(
new ClaudeMessageImageContent(new ClaudeBase64Source(
callParameters.getImageMediaType(),
callParameters.getImageData())),
new ClaudeMessageTextContent(callParameters.getMessage().getPrompt()))));
} else {
messages.add(
new ClaudeCompletionStandardMessage("user", callParameters.getMessage().getPrompt()));
}
request.setMessages(messages);
return request;
}
@ -263,22 +288,48 @@ public class CompletionRequestProvider {
var message = callParameters.getMessage();
var messages = new ArrayList<OpenAIChatCompletionMessage>();
if (callParameters.getConversationType() == ConversationType.DEFAULT) {
messages.add(new OpenAIChatCompletionMessage(
messages.add(new OpenAIChatCompletionStandardMessage(
"system",
ConfigurationSettings.getCurrentState().getSystemPrompt()));
}
if (callParameters.getConversationType() == ConversationType.FIX_COMPILE_ERRORS) {
messages.add(new OpenAIChatCompletionMessage("system", FIX_COMPILE_ERRORS_SYSTEM_PROMPT));
messages.add(
new OpenAIChatCompletionStandardMessage("system", FIX_COMPILE_ERRORS_SYSTEM_PROMPT));
}
for (var prevMessage : conversation.getMessages()) {
if (callParameters.isRetry() && prevMessage.getId().equals(message.getId())) {
break;
}
messages.add(new OpenAIChatCompletionMessage("user", prevMessage.getPrompt()));
messages.add(new OpenAIChatCompletionMessage("assistant", prevMessage.getResponse()));
var prevMessageImageFilePath = prevMessage.getImageFilePath();
if (prevMessageImageFilePath != null && !prevMessageImageFilePath.isEmpty()) {
try {
var imageFilePath = Path.of(prevMessageImageFilePath);
var imageData = Files.readAllBytes(imageFilePath);
var imageMediaType = FileUtil.getImageMediaType(imageFilePath.getFileName().toString());
messages.add(new OpenAIChatCompletionDetailedMessage("user",
List.of(
new OpenAIMessageImageURLContent(new OpenAIImageUrl(imageMediaType, imageData)),
new OpenAIMessageTextContent(prevMessage.getPrompt()))));
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
messages.add(new OpenAIChatCompletionStandardMessage("user", prevMessage.getPrompt()));
}
messages.add(new OpenAIChatCompletionStandardMessage("assistant", prevMessage.getResponse()));
}
if (callParameters.getImageMediaType() != null && callParameters.getImageData().length > 0) {
messages.add(new OpenAIChatCompletionDetailedMessage("user",
List.of(
new OpenAIMessageImageURLContent(
new OpenAIImageUrl(callParameters.getImageMediaType(),
callParameters.getImageData())),
new OpenAIMessageTextContent(message.getPrompt()))));
} else {
messages.add(new OpenAIChatCompletionStandardMessage("user", message.getPrompt()));
}
messages.add(new OpenAIChatCompletionMessage("user", message.getPrompt()));
return messages;
}
@ -324,8 +375,11 @@ public class CompletionRequestProvider {
break;
}
totalUsage -= encodingManager.countMessageTokens(messages.get(i));
messages.set(i, null);
var message = messages.get(i);
if (message instanceof OpenAIChatCompletionStandardMessage) {
totalUsage -= encodingManager.countMessageTokens(message);
messages.set(i, null);
}
}
return messages.stream().filter(Objects::nonNull).collect(toList());

View file

@ -26,11 +26,11 @@ import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings;
import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings;
import ee.carlrobert.llm.client.DeserializationUtil;
import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequest;
import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequestMessage;
import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionStandardMessage;
import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest;
import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionEventSourceListener;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage;
import ee.carlrobert.llm.client.openai.completion.response.OpenAIChatCompletionResponse;
import ee.carlrobert.llm.completion.CompletionEventListener;
import java.io.IOException;
@ -117,8 +117,8 @@ public final class CompletionRequestService {
var configuration = ConfigurationSettings.getCurrentState();
var commitMessagePrompt = configuration.getCommitMessagePrompt();
var openaiRequest = new OpenAIChatCompletionRequest.Builder(List.of(
new OpenAIChatCompletionMessage("system", commitMessagePrompt),
new OpenAIChatCompletionMessage("user", prompt)))
new OpenAIChatCompletionStandardMessage("system", commitMessagePrompt),
new OpenAIChatCompletionStandardMessage("user", prompt)))
.setModel(OpenAISettings.getCurrentState().getModel())
.build();
var selectedService = GeneralSettings.getCurrentState().getSelectedService();
@ -142,8 +142,7 @@ public final class CompletionRequestService {
claudeRequest.setStream(true);
claudeRequest.setMaxTokens(configuration.getMaxTokens());
claudeRequest.setModel(anthropicSettings.getModel());
claudeRequest.setMessages(
List.of(new ClaudeCompletionRequestMessage("user", prompt)));
claudeRequest.setMessages(List.of(new ClaudeCompletionStandardMessage("user", prompt)));
CompletionClientProvider.getClaudeClient()
.getCompletionAsync(claudeRequest, eventListener);
break;

View file

@ -6,6 +6,7 @@ import ee.carlrobert.llm.client.you.completion.YouSerpResult;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
public class Message {
@ -15,6 +16,7 @@ public class Message {
private String userMessage;
private List<YouSerpResult> serpResults;
private List<String> referencedFilePaths;
private @Nullable String imageFilePath;
public Message(String prompt, String response) {
this(prompt);
@ -71,6 +73,14 @@ public class Message {
this.referencedFilePaths = referencedFilePaths;
}
public @Nullable String getImageFilePath() {
return imageFilePath;
}
public void setImageFilePath(@Nullable String imageFilePath) {
this.imageFilePath = imageFilePath;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {

View file

@ -44,6 +44,7 @@ public class ConfigurationComponent {
private final JPanel mainPanel;
private final JBTable table;
private final JBCheckBox checkForPluginUpdatesCheckBox;
private final JBCheckBox checkForNewScreenshotsCheckBox;
private final JBCheckBox openNewTabCheckBox;
private final JBCheckBox methodNameGenerationCheckBox;
private final JBCheckBox autoFormattingCheckBox;
@ -111,6 +112,9 @@ public class ConfigurationComponent {
checkForPluginUpdatesCheckBox = new JBCheckBox(
CodeGPTBundle.get("configurationConfigurable.checkForPluginUpdates.label"),
configuration.isCheckForNewScreenshots());
checkForNewScreenshotsCheckBox = new JBCheckBox(
CodeGPTBundle.get("configurationConfigurable.checkForNewScreenshots.label"),
configuration.isCheckForPluginUpdates());
openNewTabCheckBox = new JBCheckBox(
CodeGPTBundle.get("configurationConfigurable.openNewTabCheckBox.label"),
@ -126,6 +130,7 @@ public class ConfigurationComponent {
.addComponent(tablePanel)
.addVerticalGap(4)
.addComponent(checkForPluginUpdatesCheckBox)
.addComponent(checkForNewScreenshotsCheckBox)
.addComponent(openNewTabCheckBox)
.addComponent(methodNameGenerationCheckBox)
.addComponent(autoFormattingCheckBox)
@ -152,6 +157,7 @@ public class ConfigurationComponent {
state.setSystemPrompt(systemPromptTextArea.getText());
state.setCommitMessagePrompt(commitMessagePromptTextArea.getText());
state.setCheckForPluginUpdates(checkForPluginUpdatesCheckBox.isSelected());
state.setCheckForNewScreenshots(checkForNewScreenshotsCheckBox.isSelected());
state.setCreateNewChatOnEachAction(openNewTabCheckBox.isSelected());
state.setMethodNameGenerationEnabled(methodNameGenerationCheckBox.isSelected());
state.setAutoFormattingEnabled(autoFormattingCheckBox.isSelected());
@ -168,6 +174,7 @@ public class ConfigurationComponent {
systemPromptTextArea.setText(configuration.getSystemPrompt());
commitMessagePromptTextArea.setText(configuration.getCommitMessagePrompt());
checkForPluginUpdatesCheckBox.setSelected(configuration.isCheckForPluginUpdates());
checkForNewScreenshotsCheckBox.setSelected(configuration.isCheckForNewScreenshots());
openNewTabCheckBox.setSelected(configuration.isCreateNewChatOnEachAction());
methodNameGenerationCheckBox.setSelected(configuration.isMethodNameGenerationEnabled());
autoFormattingCheckBox.setSelected(configuration.isAutoFormattingEnabled());

View file

@ -14,6 +14,7 @@ public class ConfigurationState {
private int maxTokens = 1000;
private double temperature = 0.1;
private boolean checkForPluginUpdates = true;
private boolean checkForNewScreenshots = true;
private boolean createNewChatOnEachAction;
private boolean ignoreGitCommitTokenLimit;
private boolean methodNameGenerationEnabled = true;
@ -62,6 +63,14 @@ public class ConfigurationState {
this.createNewChatOnEachAction = createNewChatOnEachAction;
}
public boolean isCheckForNewScreenshots() {
return checkForNewScreenshots;
}
public void setCheckForNewScreenshots(boolean checkForNewScreenshots) {
this.checkForNewScreenshots = checkForNewScreenshots;
}
public Map<String, String> getTableData() {
return tableData;
}

View file

@ -6,7 +6,6 @@ import static java.lang.String.format;
import static java.util.stream.Collectors.toList;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.project.Project;
@ -27,7 +26,6 @@ import ee.carlrobert.codegpt.settings.GeneralSettings;
import ee.carlrobert.codegpt.settings.service.ServiceType;
import ee.carlrobert.codegpt.telemetry.TelemetryAction;
import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager;
import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowPanel;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatToolWindowScrollablePanel;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel;
@ -41,11 +39,15 @@ import ee.carlrobert.codegpt.util.file.FileUtil;
import java.awt.BorderLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public abstract class ChatToolWindowTabPanel implements Disposable {
@ -62,10 +64,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable {
protected abstract JComponent getLandingView();
public ChatToolWindowTabPanel(
@NotNull Project project,
@NotNull Conversation conversation,
boolean useContextualSearch) {
public ChatToolWindowTabPanel(@NotNull Project project, @NotNull Conversation conversation) {
this.project = project;
this.conversation = conversation;
conversationService = ConversationService.getInstance();
@ -98,8 +97,10 @@ public abstract class ChatToolWindowTabPanel implements Disposable {
}
public void sendMessage(Message message, ConversationType conversationType) {
Runnable runnable = () -> {
SwingUtilities.invokeLater(() -> {
var referencedFiles = project.getUserData(CodeGPTKeys.SELECTED_FILES);
var chatToolWindowPanel = project.getService(StandardChatToolWindowContentManager.class)
.tryFindChatToolWindowPanel();
if (referencedFiles != null && !referencedFiles.isEmpty()) {
var referencedFilePaths = referencedFiles.stream()
.map(ReferencedFile::getFilePath)
@ -110,26 +111,42 @@ public abstract class ChatToolWindowTabPanel implements Disposable {
totalTokensPanel.updateReferencedFilesTokens(referencedFiles);
project.getService(StandardChatToolWindowContentManager.class)
.tryFindChatToolWindowPanel()
.ifPresent(StandardChatToolWindowPanel::clearSelectedFilesNotification);
chatToolWindowPanel.ifPresent(panel -> panel.clearNotifications(project));
}
var userMessagePanel = new UserMessagePanel(project, message, this);
var attachedFilePath = CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH.get(project);
var callParameters = getCallParameters(conversationType, message, attachedFilePath);
if (callParameters.getImageData() != null) {
message.setImageFilePath(attachedFilePath);
chatToolWindowPanel.ifPresent(panel -> panel.clearNotifications(project));
userMessagePanel.displayImage(attachedFilePath);
}
var messagePanel = toolWindowScrollablePanel.addMessage(message.getId());
messagePanel.add(new UserMessagePanel(project, message, this));
messagePanel.add(userMessagePanel);
var responsePanel = createResponsePanel(message, conversationType);
messagePanel.add(responsePanel);
updateTotalTokens(message);
call(callParameters, responsePanel);
});
}
call(message, conversationType, responsePanel, false);
};
// TODO
if (ApplicationManager.getApplication().isUnitTestMode()) {
runnable.run();
} else {
SwingUtilities.invokeLater(runnable);
private CallParameters getCallParameters(
ConversationType conversationType,
Message message,
@Nullable String attachedFilePath) {
var callParameters = new CallParameters(conversation, conversationType, message, false);
if (attachedFilePath != null && !attachedFilePath.isEmpty()) {
try {
callParameters.setImageData(Files.readAllBytes(Path.of(attachedFilePath)));
callParameters.setImageMediaType(FileUtil.getImageMediaType(attachedFilePath));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return callParameters;
}
private void updateTotalTokens(Message message) {
@ -175,7 +192,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable {
if (responsePanel != null) {
message.setResponse("");
conversationService.saveMessage(conversation, message);
call(message, conversationType, responsePanel, true);
call(new CallParameters(conversation, conversationType, message, true), responsePanel);
}
totalTokensPanel.updateConversationTokens(conversation);
@ -202,11 +219,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable {
totalTokensPanel.updateConversationTokens(conversation);
}
private void call(
Message message,
ConversationType conversationType,
ResponsePanel responsePanel,
boolean retry) {
private void call(CallParameters callParameters, ResponsePanel responsePanel) {
var responseContainer = (ChatMessageResponseBody) responsePanel.getContent();
if (!CompletionRequestService.getInstance().isRequestAllowed()) {
@ -222,13 +235,13 @@ public abstract class ChatToolWindowTabPanel implements Disposable {
userPromptTextArea) {
@Override
public void handleTokensExceededPolicyAccepted() {
call(message, conversationType, responsePanel, true);
call(callParameters, responsePanel);
}
});
userPromptTextArea.setRequestHandler(requestHandler);
userPromptTextArea.setSubmitEnabled(false);
requestHandler.call(new CallParameters(conversation, conversationType, message, retry));
requestHandler.call(callParameters);
}
private void handleSubmit(String text) {
@ -257,7 +270,10 @@ public abstract class ChatToolWindowTabPanel implements Disposable {
panel.add(JBUI.Panels.simplePanel(new UserPromptTextAreaHeader(
selectedService,
totalTokensPanel,
contentManager::createNewTabPanel)), BorderLayout.NORTH);
() -> {
ConversationService.getInstance().startConversation();
contentManager.createNewTabPanel();
})), BorderLayout.NORTH);
panel.add(JBUI.Panels.simplePanel(userPromptTextArea), BorderLayout.CENTER);
return panel;
}

View file

@ -1,5 +1,8 @@
package ee.carlrobert.codegpt.toolwindow.chat.standard;
import static java.lang.String.format;
import static java.util.Collections.emptyList;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionToolbar;
@ -8,6 +11,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.SimpleToolWindowPanel;
import com.intellij.openapi.util.Disposer;
import com.intellij.util.ui.JBUI;
import ee.carlrobert.codegpt.CodeGPTKeys;
import ee.carlrobert.codegpt.ReferencedFile;
import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier;
import ee.carlrobert.codegpt.actions.toolwindow.ClearChatWindowAction;
@ -15,42 +19,78 @@ import ee.carlrobert.codegpt.actions.toolwindow.CreateNewConversationAction;
import ee.carlrobert.codegpt.actions.toolwindow.OpenInEditorAction;
import ee.carlrobert.codegpt.conversations.ConversationService;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.toolwindow.chat.ui.SelectedFilesNotification;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ToolWindowFooterNotification;
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier;
import java.awt.BorderLayout;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import javax.swing.BoxLayout;
import javax.swing.JPanel;
import org.jetbrains.annotations.NotNull;
public class StandardChatToolWindowPanel extends SimpleToolWindowPanel {
private final SelectedFilesNotification selectedFilesNotification;
private final ToolWindowFooterNotification selectedFilesNotification;
private final ToolWindowFooterNotification imageFileAttachmentNotification;
private StandardChatToolWindowTabbedPane tabbedPane;
public StandardChatToolWindowPanel(
@NotNull Project project,
@NotNull Disposable parentDisposable) {
super(true);
selectedFilesNotification = new SelectedFilesNotification(project);
init(project, selectedFilesNotification, parentDisposable);
selectedFilesNotification = new ToolWindowFooterNotification(
() -> clearSelectedFilesNotification(project));
imageFileAttachmentNotification = new ToolWindowFooterNotification(() ->
project.putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, ""));
init(project, parentDisposable);
project.getMessageBus()
.connect()
.subscribe(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC,
(IncludeFilesInContextNotifier) this::displaySelectedFilesNotification);
project.getMessageBus()
.connect()
.subscribe(AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC,
(AttachImageNotifier) filePath -> imageFileAttachmentNotification.show(
Path.of(filePath).getFileName().toString(),
"File path: " + filePath));
}
public StandardChatToolWindowTabbedPane getChatTabbedPane() {
return tabbedPane;
}
public void displaySelectedFilesNotification(List<ReferencedFile> referencedFiles) {
selectedFilesNotification.displaySelectedFilesNotification(referencedFiles);
if (referencedFiles.isEmpty()) {
return;
}
var referencedFilePaths = referencedFiles.stream()
.map(ReferencedFile::getFilePath)
.collect(Collectors.toList());
selectedFilesNotification.show(
referencedFiles.size() + " files selected",
selectedFilesNotificationDescription(referencedFilePaths));
}
public void clearSelectedFilesNotification() {
selectedFilesNotification.clearSelectedFilesNotification();
private String selectedFilesNotificationDescription(List<String> referencedFilePaths) {
var html = referencedFilePaths.stream()
.map(filePath -> format("<li>%s</li>", Paths.get(filePath).getFileName().toString()))
.collect(Collectors.joining());
return format("<ul style=\"margin: 4px 12px;\">%s</ul>", html);
}
private void init(
Project project,
SelectedFilesNotification selectedFilesNotification,
Disposable parentDisposable) {
public void clearNotifications(Project project) {
selectedFilesNotification.hideNotification();
imageFileAttachmentNotification.hideNotification();
project.putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, "");
project.putUserData(CodeGPTKeys.SELECTED_FILES, emptyList());
}
private void init(Project project, Disposable parentDisposable) {
var conversation = ConversationsState.getCurrentConversation();
if (conversation == null) {
conversation = ConversationService.getInstance().startConversation();
@ -71,8 +111,11 @@ public class StandardChatToolWindowPanel extends SimpleToolWindowPanel {
BorderLayout.LINE_START);
setToolbar(actionToolbarPanel);
setContent(
JBUI.Panels.simplePanel(tabbedPane).addToBottom(selectedFilesNotification));
var notificationContainer = new JPanel(new BorderLayout());
notificationContainer.setLayout(new BoxLayout(notificationContainer, BoxLayout.PAGE_AXIS));
notificationContainer.add(selectedFilesNotification);
notificationContainer.add(imageFileAttachmentNotification);
setContent(JBUI.Panels.simplePanel(tabbedPane).addToBottom(notificationContainer));
Disposer.register(parentDisposable, tabPanel);
}
@ -102,7 +145,10 @@ public class StandardChatToolWindowPanel extends SimpleToolWindowPanel {
return tabbedPane;
}
public StandardChatToolWindowTabbedPane getChatTabbedPane() {
return tabbedPane;
public void clearSelectedFilesNotification(Project project) {
project.putUserData(CodeGPTKeys.SELECTED_FILES, emptyList());
project.getMessageBus()
.syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC)
.filesIncluded(emptyList());
}
}

View file

@ -23,7 +23,7 @@ public class StandardChatToolWindowTabPanel extends ChatToolWindowTabPanel {
public StandardChatToolWindowTabPanel(
@NotNull Project project,
@NotNull Conversation conversation) {
super(project, conversation, false);
super(project, conversation);
if (conversation.getMessages().isEmpty()) {
displayLandingView();
} else {
@ -67,8 +67,14 @@ public class StandardChatToolWindowTabPanel extends ChatToolWindowTabPanel {
}
messageResponseBody.hideCaret();
var userMessagePanel = new UserMessagePanel(project, message, this);
var imageFilePath = message.getImageFilePath();
if (imageFilePath != null && !imageFilePath.isEmpty()) {
userMessagePanel.displayImage(imageFilePath);
}
var messagePanel = toolWindowScrollablePanel.addMessage(message.getId());
messagePanel.add(new UserMessagePanel(project, message, this));
messagePanel.add(userMessagePanel);
messagePanel.add(new ResponsePanel()
.withReloadAction(() -> reloadMessage(message, conversation, ConversationType.DEFAULT))
.withDeleteAction(() -> removeMessage(message.getId(), conversation))

View file

@ -0,0 +1,86 @@
package ee.carlrobert.codegpt.toolwindow.chat.ui;
import static com.intellij.util.ui.JBUI.Panels.simplePanel;
import com.intellij.icons.AllIcons.General;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.ui.JBUI;
import ee.carlrobert.codegpt.CodeGPTBundle;
import java.awt.BorderLayout;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.ItemEvent;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.JToggleButton;
import javax.swing.SwingConstants;
public class ImageAccordion extends JPanel {
public ImageAccordion(String fileName, byte[] imageData) {
super(new BorderLayout());
setOpaque(false);
var contentPanel = createContentPanel(fileName, imageData);
add(createToggleButton(contentPanel), BorderLayout.NORTH);
add(contentPanel, BorderLayout.CENTER);
}
private JPanel createContentPanel(String fileName, byte[] imageData) {
var panel = new JPanel();
panel.setOpaque(false);
panel.setVisible(true);
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.setBorder(JBUI.Borders.empty(4, 0));
try {
ByteArrayInputStream inputStream = new ByteArrayInputStream(imageData);
BufferedImage originalImage = ImageIO.read(inputStream);
int maxHeight = 80;
BufferedImage resizedImage = resizeImage(originalImage, maxHeight);
panel.add(simplePanel()
.andTransparent()
.addToTop(
new JBLabel("<html><small><strong>%s</strong></small></html>".formatted(fileName))
.withBorder(JBUI.Borders.emptyBottom(4)))
.addToLeft(new JBLabel(new ImageIcon(resizedImage))), BorderLayout.LINE_START);
} catch (IOException e) {
panel.add(new JBLabel("ERROR: Something went wrong while reading the image"));
throw new RuntimeException(e);
}
return panel;
}
private JToggleButton createToggleButton(JPanel contentPane) {
var accordionToggle = new JToggleButton(
CodeGPTBundle.get("imageAccordion.title"),
General.ArrowDown);
accordionToggle.setFocusPainted(false);
accordionToggle.setContentAreaFilled(false);
accordionToggle.setBackground(getBackground());
accordionToggle.setSelectedIcon(General.ArrowUp);
accordionToggle.setBorder(null);
accordionToggle.setSelected(true);
accordionToggle.setHorizontalAlignment(SwingConstants.LEADING);
accordionToggle.setHorizontalTextPosition(SwingConstants.LEADING);
accordionToggle.addItemListener(e ->
contentPane.setVisible(e.getStateChange() == ItemEvent.SELECTED));
return accordionToggle;
}
private BufferedImage resizeImage(BufferedImage originalImage, int maxHeight) {
double aspectRatio = (double) originalImage.getWidth() / originalImage.getHeight();
int newWidth = (int) (maxHeight * aspectRatio);
Image resizedImage = originalImage.getScaledInstance(newWidth, maxHeight, Image.SCALE_SMOOTH);
BufferedImage bufferedResizedImage = new BufferedImage(newWidth, maxHeight,
BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = bufferedResizedImage.createGraphics();
g2d.drawImage(resizedImage, 0, 0, null);
g2d.dispose();
return bufferedResizedImage;
}
}

View file

@ -1,86 +0,0 @@
package ee.carlrobert.codegpt.toolwindow.chat.ui;
import static java.lang.String.format;
import static java.util.Collections.emptyList;
import com.intellij.icons.AllIcons.General;
import com.intellij.openapi.project.Project;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.ActionLink;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.JBUI.CurrentTheme.NotificationInfo;
import ee.carlrobert.codegpt.CodeGPTKeys;
import ee.carlrobert.codegpt.ReferencedFile;
import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier;
import java.awt.BorderLayout;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import org.jetbrains.annotations.NotNull;
public class SelectedFilesNotification extends JPanel {
private final Project project;
private final JBLabel label;
public SelectedFilesNotification(@NotNull Project project) {
super(new BorderLayout());
this.project = project;
this.label = new JBLabel(
getSelectedFilesLabel(),
General.BalloonInformation,
SwingConstants.LEADING);
setVisible(false);
setBorder(JBUI.Borders.compound(
JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0),
JBUI.Borders.empty(8, 12)));
setBackground(NotificationInfo.backgroundColor());
setForeground(NotificationInfo.foregroundColor());
add(label, BorderLayout.LINE_START);
add(new ActionLink("Remove", (event) -> {
clearSelectedFilesNotification();
}), BorderLayout.LINE_END);
}
public void displaySelectedFilesNotification(@NotNull List<ReferencedFile> referencedFiles) {
if (referencedFiles.isEmpty()) {
return;
}
label.setText(referencedFiles.size() + " files selected");
var referencedFilePaths = referencedFiles.stream()
.map(ReferencedFile::getFilePath)
.collect(Collectors.toList());
label.setToolTipText(getHtml(referencedFilePaths));
setVisible(true);
}
public void clearSelectedFilesNotification() {
project.putUserData(CodeGPTKeys.SELECTED_FILES, emptyList());
project.getMessageBus()
.syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC)
.filesIncluded(emptyList());
label.setText("0 files selected");
label.setToolTipText(null);
setVisible(false);
}
private String getHtml(List<String> referencedFilePaths) {
var html = referencedFilePaths.stream()
.map(filePath -> format("<li>%s</li>", Paths.get(filePath).getFileName().toString()))
.collect(Collectors.joining());
return format("<ul style=\"margin: 4px 12px;\">%s</ul>", html);
}
private String getSelectedFilesLabel() {
var selectedFiles = project.getUserData(CodeGPTKeys.SELECTED_FILES);
var fileCount = selectedFiles == null ? 0 : selectedFiles.size();
return fileCount + " files selected";
}
}

View file

@ -0,0 +1,50 @@
package ee.carlrobert.codegpt.toolwindow.chat.ui;
import com.intellij.icons.AllIcons.General;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.ActionLink;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.JBUI.CurrentTheme.NotificationInfo;
import java.awt.BorderLayout;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
public class ToolWindowFooterNotification extends JPanel {
private final JBLabel label;
public ToolWindowFooterNotification(Runnable onRemove) {
this("", onRemove);
}
public ToolWindowFooterNotification(String text, Runnable onRemove) {
super(new BorderLayout());
this.label = new JBLabel(text, General.BalloonInformation, SwingConstants.LEADING);
setVisible(false);
setBorder(JBUI.Borders.compound(
JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0),
JBUI.Borders.empty(8, 12)));
setBackground(NotificationInfo.backgroundColor());
setForeground(NotificationInfo.foregroundColor());
add(label, BorderLayout.LINE_START);
add(new ActionLink("Remove", (event) -> {
hideNotification();
onRemove.run();
}), BorderLayout.LINE_END);
}
public void show(String text, String toolTipText) {
label.setText(text);
label.setToolTipText(toolTipText);
setVisible(true);
}
public void hideNotification() {
label.setText("");
label.setToolTipText(null);
setVisible(false);
}
}

View file

@ -1,5 +1,6 @@
package ee.carlrobert.codegpt.toolwindow.chat.ui;
import com.intellij.icons.AllIcons.General;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.project.Project;
import com.intellij.ui.ColorUtil;
@ -11,6 +12,9 @@ import ee.carlrobert.codegpt.Icons;
import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.settings.GeneralSettings;
import java.awt.BorderLayout;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
@ -39,6 +43,18 @@ public class UserMessagePanel extends JPanel {
}
}
public void displayImage(String imageFilePath) {
try {
var path = Paths.get(imageFilePath);
add(new ImageAccordion(path.getFileName().toString(), Files.readAllBytes(path)));
} catch (IOException e) {
add(new JBLabel(
"<html><small>Unable to load image %s</small></html>".formatted(imageFilePath),
General.Error,
SwingConstants.LEFT));
}
}
private ChatMessageResponseBody createResponseBody(
Project project,
String prompt,

View file

@ -0,0 +1,11 @@
package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea;
import com.intellij.util.messages.Topic;
public interface AttachImageNotifier {
Topic<AttachImageNotifier> IMAGE_ATTACHMENT_FILE_PATH_TOPIC =
Topic.create("imageAttachmentFilePath", AttachImageNotifier.class);
void imageAttached(String filePath);
}

View file

@ -18,8 +18,6 @@ import ee.carlrobert.codegpt.Icons;
import ee.carlrobert.codegpt.completions.llama.LlamaModel;
import ee.carlrobert.codegpt.completions.you.YouUserManager;
import ee.carlrobert.codegpt.completions.you.auth.SignedOutNotifier;
import ee.carlrobert.codegpt.conversations.ConversationService;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.settings.GeneralSettings;
import ee.carlrobert.codegpt.settings.GeneralSettingsState;
import ee.carlrobert.codegpt.settings.service.ServiceType;
@ -39,13 +37,13 @@ import org.jetbrains.annotations.NotNull;
public class ModelComboBoxAction extends ComboBoxAction {
private final Runnable onAddNewTab;
private final Runnable onModelChange;
private final GeneralSettingsState settings;
private final OpenAISettingsState openAISettings;
private final YouSettingsState youSettings;
public ModelComboBoxAction(Runnable onAddNewTab, ServiceType selectedService) {
this.onAddNewTab = onAddNewTab;
public ModelComboBoxAction(Runnable onModelChange, ServiceType selectedService) {
this.onModelChange = onModelChange;
settings = GeneralSettings.getCurrentState();
openAISettings = OpenAISettings.getCurrentState();
youSettings = YouSettings.getCurrentState();
@ -74,6 +72,7 @@ public class ModelComboBoxAction extends ComboBoxAction {
var actionGroup = new DefaultActionGroup();
actionGroup.addSeparator("OpenAI");
List.of(
OpenAIChatCompletionModel.GPT_4_VISION_PREVIEW,
OpenAIChatCompletionModel.GPT_4_0125_128k,
OpenAIChatCompletionModel.GPT_3_5_0125_16k,
OpenAIChatCompletionModel.GPT_4_32k,
@ -210,7 +209,7 @@ public class ModelComboBoxAction extends ComboBoxAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
handleProviderChange(serviceType, label, icon, comboBoxPresentation);
handleModelChange(serviceType, label, icon, comboBoxPresentation);
}
@Override
@ -220,7 +219,7 @@ public class ModelComboBoxAction extends ComboBoxAction {
};
}
private void handleProviderChange(
private void handleModelChange(
ServiceType serviceType,
String label,
Icon icon,
@ -228,13 +227,7 @@ public class ModelComboBoxAction extends ComboBoxAction {
settings.setSelectedService(serviceType);
comboBoxPresentation.setIcon(icon);
comboBoxPresentation.setText(label);
var currentConversation = ConversationsState.getCurrentConversation();
if (currentConversation != null && !currentConversation.getMessages().isEmpty()) {
onAddNewTab.run();
} else {
ConversationService.getInstance().startConversation();
}
onModelChange.run();
}
private AnAction createOpenAIModelAction(
@ -252,7 +245,7 @@ public class ModelComboBoxAction extends ComboBoxAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
openAISettings.setModel(model.getCode());
handleProviderChange(
handleModelChange(
OPENAI,
model.getDescription(),
Icons.OpenAI,
@ -281,7 +274,7 @@ public class ModelComboBoxAction extends ComboBoxAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
youSettings.setChatMode(mode);
handleProviderChange(
handleModelChange(
YOU,
mode.getDescription(),
Icons.YouSmall,
@ -311,7 +304,7 @@ public class ModelComboBoxAction extends ComboBoxAction {
public void actionPerformed(@NotNull AnActionEvent e) {
youSettings.setCustomModel(model);
youSettings.setChatMode(YouCompletionMode.CUSTOM);
handleProviderChange(
handleModelChange(
YOU,
model.getDescription(),
Icons.YouSmall,

View file

@ -1,6 +1,12 @@
package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea;
import static ee.carlrobert.codegpt.settings.service.ServiceType.ANTHROPIC;
import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI;
import static ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel.GPT_4_VISION_PREVIEW;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.util.registry.Registry;
@ -10,11 +16,14 @@ import com.intellij.ui.components.JBTextArea;
import com.intellij.util.ui.JBUI;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.Icons;
import ee.carlrobert.codegpt.actions.AttachImageAction;
import ee.carlrobert.codegpt.completions.CompletionRequestHandler;
import ee.carlrobert.codegpt.settings.GeneralSettings;
import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings;
import ee.carlrobert.codegpt.ui.IconActionButton;
import ee.carlrobert.codegpt.ui.UIUtil;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
@ -23,16 +32,14 @@ import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.text.BadLocationException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class UserPromptTextArea extends JPanel {
@ -41,11 +48,12 @@ public class UserPromptTextArea extends JPanel {
private static final JBColor BACKGROUND_COLOR = JBColor.namedColor(
"Editor.SearchField.background", com.intellij.util.ui.UIUtil.getTextFieldBackground());
private final AtomicReference<CompletionRequestHandler> requestHandlerRef =
new AtomicReference<>();
private final JBTextArea textArea;
private final int textAreaRadius = 16;
private final Consumer<String> onSubmit;
private JButton stopButton;
private IconActionButton stopButton;
private JPanel iconsPanel;
private boolean submitEnabled = true;
@ -150,6 +158,10 @@ public class UserPromptTextArea extends JPanel {
stopButton.setEnabled(!submitEnabled);
}
public void setRequestHandler(@NotNull CompletionRequestHandler handler) {
requestHandlerRef.set(handler);
}
private void handleSubmit() {
if (submitEnabled && !textArea.getText().isEmpty()) {
// Replacing each newline with two newlines to ensure proper Markdown formatting
@ -163,12 +175,34 @@ public class UserPromptTextArea extends JPanel {
setOpaque(false);
add(textArea, BorderLayout.CENTER);
stopButton = createIconButton(AllIcons.Actions.Suspend, null);
stopButton = new IconActionButton(
new AnAction("Stop", "Stop current inference", AllIcons.Actions.Suspend) {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
var handler = requestHandlerRef.get();
if (handler != null) {
handler.cancel();
}
}
});
stopButton.setEnabled(false);
var flowLayout = new FlowLayout(FlowLayout.RIGHT);
flowLayout.setHgap(8);
iconsPanel = new JPanel(flowLayout);
iconsPanel.add(createIconButton(Icons.Send, this::handleSubmit));
iconsPanel.add(new IconActionButton(
new AnAction("Send Message", "Send message", Icons.Send) {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
handleSubmit();
}
}));
var selectedService = GeneralSettings.getCurrentState().getSelectedService();
if (selectedService == ANTHROPIC
|| (selectedService == OPENAI
&& GPT_4_VISION_PREVIEW.getCode().equals(OpenAISettings.getCurrentState().getModel()))) {
iconsPanel.add(new IconActionButton(new AttachImageAction()));
}
iconsPanel.add(stopButton);
add(iconsPanel, BorderLayout.EAST);
}
@ -180,19 +214,4 @@ public class UserPromptTextArea extends JPanel {
textArea.setFont(UIManager.getFont("TextField.font"));
}
}
// TODO: IconActionButton?
private JButton createIconButton(Icon icon, @Nullable Runnable submitListener) {
var button = UIUtil.createIconButton(icon);
if (submitListener != null) {
button.addActionListener((e) -> handleSubmit());
}
button.setCursor(new Cursor(Cursor.HAND_CURSOR));
button.setEnabled(false);
return button;
}
public void setRequestHandler(@NotNull CompletionRequestHandler requestService) {
stopButton.addActionListener(e -> requestService.cancel());
}
}

View file

@ -1,12 +1,7 @@
package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.JBUI;
import ee.carlrobert.codegpt.completions.you.YouSubscriptionNotifier;
import ee.carlrobert.codegpt.completions.you.auth.SignedOutNotifier;
import ee.carlrobert.codegpt.settings.service.ServiceType;
import java.awt.BorderLayout;
import javax.swing.JPanel;
@ -16,7 +11,7 @@ public class UserPromptTextAreaHeader extends JPanel {
public UserPromptTextAreaHeader(
ServiceType selectedService,
TotalTokensPanel totalTokensPanel,
Runnable onAddNewTab) {
Runnable onModelChange) {
super(new BorderLayout());
setOpaque(false);
setBorder(JBUI.Borders.emptyBottom(8));
@ -29,29 +24,7 @@ public class UserPromptTextAreaHeader extends JPanel {
break;
default:
}
add(new ModelComboBoxAction(onAddNewTab, selectedService)
add(new ModelComboBoxAction(onModelChange, selectedService)
.createCustomComponent(ActionPlaces.UNKNOWN), BorderLayout.LINE_END);
}
private void subscribeToYouTopics(JBCheckBox gpt4CheckBox) {
var messageBusConnection = ApplicationManager.getApplication().getMessageBus().connect();
subscribeToYouSubscriptionTopic(messageBusConnection, gpt4CheckBox);
subscribeToSignedOutTopic(messageBusConnection, gpt4CheckBox);
}
private void subscribeToSignedOutTopic(
MessageBusConnection messageBusConnection,
JBCheckBox gpt4CheckBox) {
messageBusConnection.subscribe(
SignedOutNotifier.SIGNED_OUT_TOPIC,
(SignedOutNotifier) () -> gpt4CheckBox.setEnabled(false));
}
private void subscribeToYouSubscriptionTopic(
MessageBusConnection messageBusConnection,
JBCheckBox gpt4CheckBox) {
messageBusConnection.subscribe(
YouSubscriptionNotifier.SUBSCRIPTION_TOPIC,
(YouSubscriptionNotifier) () -> gpt4CheckBox.setEnabled(true));
}
}

View file

@ -141,6 +141,15 @@ public class FileUtil {
}
}
public static String getImageMediaType(String fileName) {
var fileExtension = getFileExtension(fileName);
return switch (fileExtension) {
case "png" -> "image/png";
case "jpg", "jpeg" -> "image/jpeg";
default -> throw new IllegalArgumentException("Unsupported image type: " + fileExtension);
};
}
public static String getResourceContent(String name) {
try (var stream = Objects.requireNonNull(FileUtil.class.getResourceAsStream(name))) {
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);

View file

@ -1,9 +1,11 @@
package ee.carlrobert.codegpt
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.openapi.util.Disposer
import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil
import ee.carlrobert.codegpt.completions.you.YouUserManager
import ee.carlrobert.codegpt.completions.you.auth.AuthenticationHandler
@ -13,8 +15,11 @@ import ee.carlrobert.codegpt.completions.you.auth.response.YouAuthenticationResp
import ee.carlrobert.codegpt.credentials.CredentialsStore
import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey
import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.settings.service.you.YouSettings
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier
import ee.carlrobert.codegpt.ui.OverlayUtil
import java.nio.file.Paths
class CodeGPTProjectActivity : ProjectActivity {
@ -23,12 +28,24 @@ class CodeGPTProjectActivity : ProjectActivity {
CredentialsStore.loadAll()
if (YouUserManager.getInstance().authenticationResponse == null) {
ApplicationManager.getApplication()
.executeOnPooledThread { this.handleYouServiceAuthentication() }
handleYouServiceAuthenticationAsync()
}
if (!ApplicationManager.getApplication().isUnitTestMode
&& ConfigurationSettings.getCurrentState().isCheckForNewScreenshots
) {
val pathToWatch = Paths.get(System.getProperty("user.home"), "Desktop")
val fileWatcher = FileWatcher(pathToWatch)
fileWatcher.watch {
if (listOf("jpg", "jpeg", "png").contains(it.extension)) {
showImageAttachmentNotification(project, it.absolutePath)
}
}
Disposer.register(project, fileWatcher)
}
}
private fun handleYouServiceAuthentication() {
private fun handleYouServiceAuthenticationAsync() {
val settings = YouSettings.getCurrentState()
val password = getCredential(CredentialKey.YOU_ACCOUNT_PASSWORD)
if (settings.email.isNotEmpty() && !password.isNullOrEmpty()) {
@ -57,4 +74,27 @@ class CodeGPTProjectActivity : ProjectActivity {
})
}
}
private fun showImageAttachmentNotification(project: Project, filePath: String) {
OverlayUtil.getDefaultNotification(
CodeGPTBundle.get("imageAttachmentNotification.content"),
NotificationType.INFORMATION
)
.addAction(NotificationAction.createSimpleExpiring(
CodeGPTBundle.get("imageAttachmentNotification.action")
) {
CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH.set(project, filePath)
project.messageBus
.syncPublisher<AttachImageNotifier>(
AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC
)
.imageAttached(filePath)
})
.addAction(NotificationAction.createSimpleExpiring(
CodeGPTBundle.get("shared.notification.doNotShowAgain")
) {
ConfigurationSettings.getCurrentState().isCheckForNewScreenshots = false
})
.notify(project)
}
}

View file

@ -1,4 +1,4 @@
package ee.carlrobert.codegpt;
package ee.carlrobert.codegpt
import com.intellij.ide.plugins.InstalledPluginsState
import com.intellij.notification.NotificationAction
@ -39,7 +39,7 @@ class CodeGPTUpdateActivity : ProjectActivity {
Task.Backgroundable(project, CodeGPTBundle.get("checkForUpdatesTask.title"), true) {
override fun run(indicator: ProgressIndicator) {
val isLatestVersion =
!InstalledPluginsState.getInstance().hasNewerVersion(CodeGPTPlugin.CODEGPT_ID);
!InstalledPluginsState.getInstance().hasNewerVersion(CodeGPTPlugin.CODEGPT_ID)
if (project.isDisposed || isLatestVersion) {
return
}
@ -55,7 +55,7 @@ class CodeGPTUpdateActivity : ProjectActivity {
.executeOnPooledThread { installCodeGPTUpdate(project) }
})
.addAction(NotificationAction.createSimpleExpiring(
CodeGPTBundle.get("checkForUpdatesTask.notification.hideButton")
CodeGPTBundle.get("shared.notification.doNotShowAgain")
) {
ConfigurationSettings.getCurrentState().isCheckForPluginUpdates = false
})

View file

@ -0,0 +1,29 @@
package ee.carlrobert.codegpt
import com.intellij.openapi.Disposable
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor
import org.apache.commons.io.monitor.FileAlterationMonitor
import org.apache.commons.io.monitor.FileAlterationObserver
import java.io.File
import java.nio.file.Path
class FileWatcher(private val pathToWatch: Path) : Disposable {
private val fileMonitor =
FileAlterationMonitor(500, FileAlterationObserver(pathToWatch.toFile()))
fun watch(onFileCreated: (File) -> Unit) {
val observer = FileAlterationObserver(pathToWatch.toFile())
observer.addListener(object : FileAlterationListenerAdaptor() {
override fun onFileCreate(file: File) {
onFileCreated(file)
}
})
fileMonitor.addObserver(observer)
fileMonitor.start()
}
override fun dispose() {
fileMonitor.stop()
}
}

View file

@ -0,0 +1,40 @@
package ee.carlrobert.codegpt.actions
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.CodeGPTKeys
import ee.carlrobert.codegpt.Icons
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier
class AttachImageAction : AnAction(
CodeGPTBundle.get("action.attachImage"),
CodeGPTBundle.get("action.attachImageDescription"),
Icons.Upload
) {
override fun actionPerformed(e: AnActionEvent) {
FileChooser.chooseFiles(createSingleImageFileDescriptor(), e.project, null).also { files ->
if (files.isNotEmpty()) {
check(files.size == 1) { "Expected exactly one file to be selected" }
e.project?.let { project ->
CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH[project] = files.first().path
project.messageBus
.syncPublisher(AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC)
.imageAttached(files.first().path)
}
}
}
}
private fun createSingleImageFileDescriptor() = FileChooserDescriptor(
true, false, false, false, false, false
).apply {
withFileFilter { file ->
file.extension in listOf("jpg", "jpeg", "png")
}
withTitle(CodeGPTBundle.get("imageFileChooser.title"))
}
}

View file

@ -1,6 +1,6 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="20px" height="20px" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#7a8183">
<svg width="18px" height="18px" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#7a8183">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path d="M10.5004 11.9998H5.00043M4.91577 12.2913L2.58085 19.266C2.39742 19.8139 2.3057 20.0879 2.37152 20.2566C2.42868 20.4031 2.55144 20.5142 2.70292 20.5565C2.87736 20.6052 3.14083 20.4866 3.66776 20.2495L20.3792 12.7293C20.8936 12.4979 21.1507 12.3822 21.2302 12.2214C21.2993 12.0817 21.2993 11.9179 21.2302 11.7782C21.1507 11.6174 20.8936 11.5017 20.3792 11.2703L3.66193 3.74751C3.13659 3.51111 2.87392 3.39291 2.69966 3.4414C2.54832 3.48351 2.42556 3.59429 2.36821 3.74054C2.30216 3.90893 2.3929 4.18231 2.57437 4.72906L4.91642 11.7853C4.94759 11.8792 4.96317 11.9262 4.96933 11.9742C4.97479 12.0168 4.97473 12.0599 4.96916 12.1025C4.96289 12.1506 4.94718 12.1975 4.91577 12.2913Z" stroke="#7a8183" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> </g>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

@ -1,6 +1,6 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="20px" height="20px" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff">
<svg width="18px" height="18px" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path d="M10.5004 11.9998H5.00043M4.91577 12.2913L2.58085 19.266C2.39742 19.8139 2.3057 20.0879 2.37152 20.2566C2.42868 20.4031 2.55144 20.5142 2.70292 20.5565C2.87736 20.6052 3.14083 20.4866 3.66776 20.2495L20.3792 12.7293C20.8936 12.4979 21.1507 12.3822 21.2302 12.2214C21.2993 12.0817 21.2993 11.9179 21.2302 11.7782C21.1507 11.6174 20.8936 11.5017 20.3792 11.2703L3.66193 3.74751C3.13659 3.51111 2.87392 3.39291 2.69966 3.4414C2.54832 3.48351 2.42556 3.59429 2.36821 3.74054C2.30216 3.90893 2.3929 4.18231 2.57437 4.72906L4.91642 11.7853C4.94759 11.8792 4.96317 11.9262 4.96933 11.9742C4.97479 12.0168 4.97473 12.0599 4.96916 12.1025C4.96289 12.1506 4.94718 12.1975 4.91577 12.2913Z" stroke="#ffffff" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> </g>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

@ -0,0 +1,7 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8535 4.6463C11.0488 4.84156 11.0488 5.15814 10.8536 5.35341C10.6583 5.54868 10.3417 5.54868 10.1465 5.35343L8.49972 3.70677L8.49972 10.5142C8.49972 10.7903 8.27586 11.0142 7.99972 11.0142C7.72358 11.0142 7.49972 10.7903 7.49972 10.5142L7.49972 3.70729L5.85343 5.35356C5.65816 5.54882 5.34158 5.54881 5.14632 5.35355C4.95106 5.15829 4.95106 4.8417 5.14633 4.64644L7.64642 2.1464C7.84167 1.95114 8.15825 1.95114 8.35351 2.14639L10.8535 4.6463Z" fill="#6C707E"/>
<rect width="12" height="1" rx="0.5" transform="matrix(1 0 0 -1 2 14)" fill="#6C707E"/>
<rect x="2" y="10" width="1" height="4" rx="0.5" fill="#6C707E"/>
<rect x="13" y="10" width="1" height="4" rx="0.5" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 964 B

View file

@ -0,0 +1,7 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8535 4.6463C11.0488 4.84156 11.0488 5.15814 10.8536 5.35341C10.6583 5.54868 10.3417 5.54868 10.1465 5.35343L8.49972 3.70677L8.49972 10.5142C8.49972 10.7903 8.27586 11.0142 7.99972 11.0142C7.72358 11.0142 7.49972 10.7903 7.49972 10.5142L7.49972 3.70729L5.85343 5.35356C5.65816 5.54882 5.34158 5.54881 5.14632 5.35355C4.95106 5.15829 4.95106 4.8417 5.14633 4.64644L7.64642 2.1464C7.84167 1.95114 8.15825 1.95114 8.35351 2.14639L10.8535 4.6463Z" fill="#CED0D6"/>
<rect width="12" height="1" rx="0.5" transform="matrix(1 0 0 -1 2 14)" fill="#CED0D6"/>
<rect x="2" y="10" width="1" height="4" rx="0.5" fill="#CED0D6"/>
<rect x="13" y="10" width="1" height="4" rx="0.5" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 964 B

View file

@ -87,6 +87,7 @@ configurationConfigurable.table.header.promptColumnLabel=Prompt
configurationConfigurable.table.action.revertToDefaults.text=Revert to Defaults
configurationConfigurable.table.action.addKeymap.text=Add Shortcut
configurationConfigurable.checkForPluginUpdates.label=Check for plugin updates automatically
configurationConfigurable.checkForNewScreenshots.label=Check for new screenshots automatically
configurationConfigurable.openNewTabCheckBox.label=Open a new chat on each action
configurationConfigurable.enableMethodNameGeneration.label=Enable method name lookup suggestions
configurationConfigurable.autoFormatting.label=Enable automatic code formatting
@ -178,7 +179,6 @@ validation.error.mustBeGreaterThanZero=Value must be greater than 0
checkForUpdatesTask.title=Checking for CodeGPT update...
checkForUpdatesTask.notification.message=An update for CodeGPT is available.
checkForUpdatesTask.notification.installButton=Install update
checkForUpdatesTask.notification.hideButton=Do not show again
llamaServerAgent.buildingProject.description=Building llama.cpp...
llamaServerAgent.serverBootup.description=Booting up server...
notification.compilationError.description=CodeGPT has detected a compilation error. Would you like assistance in resolving it?
@ -190,4 +190,11 @@ shared.infillPromptTemplate=Infill template:
shared.apiVersion=API version:
shared.configuration=Configuration
shared.port=Port:
codeCompletion.progress.title=Code completion in progress
shared.notification.doNotShowAgain=Do not show again
codeCompletion.progress.title=Code completion in progress
imageAttachmentNotification.content=New image detected on desktop. Would you like to attach it to your current conversation?
imageAttachmentNotification.action=Attach image
action.attachImage=Attach Image
action.attachImageDescription=Attach an image
imageFileChooser.title=Select Image
imageAccordion.title=Attached image

View file

@ -39,9 +39,6 @@ public class CodeCompletionServiceTest extends IntegrationTest {
myFixture.type('c');
PlatformTestUtil.waitWithEventsDispatching(
"Editor inlay assertions failed",
() -> "TEST_OUTPUT".equals(PREVIOUS_INLAY_TEXT.get(myFixture.getEditor())),
5);
waitExpecting(() -> "TEST_OUTPUT".equals(PREVIOUS_INLAY_TEXT.get(myFixture.getEditor())));
}
}

View file

@ -6,10 +6,8 @@ import static ee.carlrobert.llm.client.util.JSONUtil.e;
import static ee.carlrobert.llm.client.util.JSONUtil.jsonArray;
import static ee.carlrobert.llm.client.util.JSONUtil.jsonMap;
import static ee.carlrobert.llm.client.util.JSONUtil.jsonMapResponse;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.http.HttpHeaders.AUTHORIZATION;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import ee.carlrobert.codegpt.CodeGPTPlugin;
import ee.carlrobert.codegpt.conversations.ConversationService;
@ -50,7 +48,7 @@ public class DefaultCompletionRequestHandlerTest extends IntegrationTest {
requestHandler.call(new CallParameters(conversation, ConversationType.DEFAULT, message, false));
await().atMost(5, SECONDS).until(() -> "Hello!".equals(message.getResponse()));
waitExpecting(() -> "Hello!".equals(message.getResponse()));
}
public void testAzureChatCompletionCall() {
@ -86,7 +84,7 @@ public class DefaultCompletionRequestHandlerTest extends IntegrationTest {
requestHandler.call(new CallParameters(conversation, ConversationType.DEFAULT, message, false));
await().atMost(5, SECONDS).until(() -> "Hello!".equals(message.getResponse()));
waitExpecting(() -> "Hello!".equals(message.getResponse()));
}
public void testYouChatCompletionCall() {
@ -137,7 +135,7 @@ public class DefaultCompletionRequestHandlerTest extends IntegrationTest {
requestHandler.call(new CallParameters(conversation, ConversationType.DEFAULT, message, false));
await().atMost(5, SECONDS).until(() -> "Hello!".equals(message.getResponse()));
waitExpecting(() -> "Hello!".equals(message.getResponse()));
}
public void testLlamaChatCompletionCall() {
@ -171,7 +169,7 @@ public class DefaultCompletionRequestHandlerTest extends IntegrationTest {
requestHandler.call(new CallParameters(conversation, ConversationType.DEFAULT, message, false));
await().atMost(5, SECONDS).until(() -> "Hello!".equals(message.getResponse()));
waitExpecting(() -> "Hello!".equals(message.getResponse()));
}
private CompletionResponseEventListener getRequestEventListener(Message message) {

View file

@ -7,10 +7,9 @@ import static ee.carlrobert.llm.client.util.JSONUtil.e;
import static ee.carlrobert.llm.client.util.JSONUtil.jsonArray;
import static ee.carlrobert.llm.client.util.JSONUtil.jsonMap;
import static ee.carlrobert.llm.client.util.JSONUtil.jsonMapResponse;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.Objects.requireNonNull;
import static org.apache.http.HttpHeaders.AUTHORIZATION;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import ee.carlrobert.codegpt.CodeGPTKeys;
import ee.carlrobert.codegpt.EncodingManager;
@ -23,6 +22,10 @@ import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings;
import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings;
import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowTabPanel;
import ee.carlrobert.llm.client.http.exchange.StreamHttpExchange;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import testsupport.IntegrationTest;
@ -57,11 +60,10 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest {
panel.sendMessage(message);
await().atMost(5, SECONDS)
.until(() -> {
var messages = conversation.getMessages();
return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse());
});
waitExpecting(() -> {
var messages = conversation.getMessages();
return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse());
});
var encodingManager = EncodingManager.getInstance();
assertThat(panel.getTokenDetails()).extracting(
"systemPromptTokens",
@ -114,23 +116,28 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest {
List.of(
Map.of("role", "system", "content", COMPLETION_SYSTEM_PROMPT),
Map.of("role", "user", "content",
"Use the following context to answer question at the end:\n\n"
+ "File Path: TEST_FILE_PATH_1\n"
+ "File Content:\n"
+ "```TEST_FILE_NAME_1\n"
+ "TEST_FILE_CONTENT_1\n"
+ "```\n\n"
+ "File Path: TEST_FILE_PATH_2\n"
+ "File Content:\n"
+ "```TEST_FILE_NAME_2\n"
+ "TEST_FILE_CONTENT_2\n"
+ "```\n\n"
+ "File Path: TEST_FILE_PATH_3\n"
+ "File Content:\n"
+ "```TEST_FILE_NAME_3\n"
+ "TEST_FILE_CONTENT_3\n"
+ "```\n\n"
+ "Question: TEST_MESSAGE")));
"""
Use the following context to answer question at the end:
File Path: TEST_FILE_PATH_1
File Content:
```TEST_FILE_NAME_1
TEST_FILE_CONTENT_1
```
File Path: TEST_FILE_PATH_2
File Content:
```TEST_FILE_NAME_2
TEST_FILE_CONTENT_2
```
File Path: TEST_FILE_PATH_3
File Content:
```TEST_FILE_NAME_3
TEST_FILE_CONTENT_3
```
Question: TEST_MESSAGE""")));
return List.of(
jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("role", "assistant")))),
jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "Hel")))),
@ -140,11 +147,10 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest {
panel.sendMessage(message);
await().atMost(5, SECONDS)
.until(() -> {
var messages = conversation.getMessages();
return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse());
});
waitExpecting(() -> {
var messages = conversation.getMessages();
return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse());
});
var encodingManager = EncodingManager.getInstance();
assertThat(panel.getTokenDetails()).extracting(
"systemPromptTokens",
@ -175,6 +181,79 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest {
List.of("TEST_FILE_PATH_1", "TEST_FILE_PATH_2", "TEST_FILE_PATH_3"));
}
public void testSendingOpenAIMessageWithImage() {
var testImagePath = requireNonNull(getClass().getResource("/images/test-image.png")).getPath();
getProject().putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, testImagePath);
useOpenAIService("gpt-4-vision-preview");
ConfigurationSettings.getCurrentState().setSystemPrompt(COMPLETION_SYSTEM_PROMPT);
var message = new Message("TEST_MESSAGE");
var conversation = ConversationService.getInstance().startConversation();
var panel = new StandardChatToolWindowTabPanel(getProject(), conversation);
expectOpenAI((StreamHttpExchange) request -> {
assertThat(request.getUri().getPath()).isEqualTo("/v1/chat/completions");
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getHeaders().get(AUTHORIZATION).get(0)).isEqualTo("Bearer TEST_API_KEY");
try {
var testImageUrl = "data:image/png;base64,"
+ Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of(testImagePath)));
assertThat(request.getBody())
.extracting("model", "messages")
.containsExactly(
"gpt-4-vision-preview",
List.of(
Map.of("role", "system", "content", COMPLETION_SYSTEM_PROMPT),
Map.of("role", "user", "content", List.of(
Map.of(
"type", "image_url",
"image_url", Map.of("url", testImageUrl)),
Map.of("type", "text", "text", "TEST_MESSAGE")
))));
} catch (IOException e) {
throw new RuntimeException(e);
}
return List.of(
jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("role", "assistant")))),
jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "Hel")))),
jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "lo")))),
jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "!")))));
});
panel.sendMessage(message);
waitExpecting(() -> {
var messages = conversation.getMessages();
return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse());
});
var encodingManager = EncodingManager.getInstance();
assertThat(panel.getTokenDetails()).extracting(
"systemPromptTokens",
"conversationTokens",
"userPromptTokens",
"highlightedTokens")
.containsExactly(
encodingManager.countTokens(COMPLETION_SYSTEM_PROMPT),
encodingManager.countTokens(message.getPrompt()),
0,
0);
assertThat(panel.getConversation())
.isNotNull()
.extracting("id", "model", "clientCode", "discardTokenLimit")
.containsExactly(
conversation.getId(),
conversation.getModel(),
conversation.getClientCode(),
false);
var messages = panel.getConversation().getMessages();
assertThat(messages.size()).isOne();
assertThat(messages.get(0))
.extracting("id", "prompt", "response", "imageFilePath")
.containsExactly(
message.getId(),
message.getPrompt(),
message.getResponse(),
message.getImageFilePath());
}
public void testFixCompileErrorsWithOpenAIService() {
getProject().putUserData(CodeGPTKeys.SELECTED_FILES, List.of(
new ReferencedFile("TEST_FILE_NAME_1", "TEST_FILE_PATH_1", "TEST_FILE_CONTENT_1"),
@ -201,23 +280,28 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest {
List.of(
Map.of("role", "system", "content", FIX_COMPILE_ERRORS_SYSTEM_PROMPT),
Map.of("role", "user", "content",
"Use the following context to answer question at the end:\n\n"
+ "File Path: TEST_FILE_PATH_1\n"
+ "File Content:\n"
+ "```TEST_FILE_NAME_1\n"
+ "TEST_FILE_CONTENT_1\n"
+ "```\n\n"
+ "File Path: TEST_FILE_PATH_2\n"
+ "File Content:\n"
+ "```TEST_FILE_NAME_2\n"
+ "TEST_FILE_CONTENT_2\n"
+ "```\n\n"
+ "File Path: TEST_FILE_PATH_3\n"
+ "File Content:\n"
+ "```TEST_FILE_NAME_3\n"
+ "TEST_FILE_CONTENT_3\n"
+ "```\n\n"
+ "Question: TEST_MESSAGE")));
"""
Use the following context to answer question at the end:
File Path: TEST_FILE_PATH_1
File Content:
```TEST_FILE_NAME_1
TEST_FILE_CONTENT_1
```
File Path: TEST_FILE_PATH_2
File Content:
```TEST_FILE_NAME_2
TEST_FILE_CONTENT_2
```
File Path: TEST_FILE_PATH_3
File Content:
```TEST_FILE_NAME_3
TEST_FILE_CONTENT_3
```
Question: TEST_MESSAGE""")));
return List.of(
jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("role", "assistant")))),
jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "Hel")))),
@ -227,11 +311,10 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest {
panel.sendMessage(message, ConversationType.FIX_COMPILE_ERRORS);
await().atMost(5, SECONDS)
.until(() -> {
var messages = conversation.getMessages();
return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse());
});
waitExpecting(() -> {
var messages = conversation.getMessages();
return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse());
});
var encodingManager = EncodingManager.getInstance();
assertThat(panel.getTokenDetails()).extracting(
"systemPromptTokens",
@ -312,11 +395,10 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest {
panel.sendMessage(message, ConversationType.DEFAULT);
await().atMost(5, SECONDS)
.until(() -> {
var messages = conversation.getMessages();
return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse());
});
waitExpecting(() -> {
var messages = conversation.getMessages();
return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse());
});
assertThat(panel.getConversation())
.isNotNull()
.extracting("id", "model", "clientCode", "discardTokenLimit")

View file

@ -1,5 +1,6 @@
package testsupport;
import com.intellij.openapi.util.Key;
import com.intellij.testFramework.fixtures.BasePlatformTestCase;
import ee.carlrobert.codegpt.CodeGPTKeys;
import ee.carlrobert.llm.client.mixin.ExternalServiceTestMixin;
@ -17,8 +18,17 @@ public class IntegrationTest extends BasePlatformTestCase implements
@Override
protected void tearDown() throws Exception {
ExternalServiceTestMixin.clearAll();
getProject().putUserData(CodeGPTKeys.SELECTED_FILES, Collections.emptyList());
getProject().putUserData(CodeGPTKeys.PREVIOUS_INLAY_TEXT, "");
clearKeys();
super.tearDown();
}
private void clearKeys() {
putUserData(CodeGPTKeys.SELECTED_FILES, Collections.emptyList());
putUserData(CodeGPTKeys.PREVIOUS_INLAY_TEXT, "");
putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, "");
}
private <T> void putUserData(Key<T> key, T value) {
getProject().putUserData(key, value);
}
}

View file

@ -3,19 +3,25 @@ package testsupport.mixin;
import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AZURE_OPENAI_API_KEY;
import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.OPENAI_API_KEY;
import com.intellij.testFramework.PlatformTestUtil;
import ee.carlrobert.codegpt.credentials.CredentialsStore;
import ee.carlrobert.codegpt.settings.GeneralSettings;
import ee.carlrobert.codegpt.settings.service.ServiceType;
import ee.carlrobert.codegpt.settings.service.azure.AzureSettings;
import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings;
import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings;
import java.util.function.BooleanSupplier;
public interface ShortcutsTestMixin {
default void useOpenAIService() {
useOpenAIService("gpt-4");
}
default void useOpenAIService(String model) {
GeneralSettings.getCurrentState().setSelectedService(ServiceType.OPENAI);
CredentialsStore.INSTANCE.setCredential(OPENAI_API_KEY, "TEST_API_KEY");
OpenAISettings.getCurrentState().setModel("gpt-4");
OpenAISettings.getCurrentState().setModel(model);
}
default void useAzureService() {
@ -35,4 +41,11 @@ public interface ShortcutsTestMixin {
GeneralSettings.getCurrentState().setSelectedService(ServiceType.LLAMA_CPP);
LlamaSettings.getCurrentState().setServerPort(null);
}
default void waitExpecting(BooleanSupplier condition) {
PlatformTestUtil.waitWithEventsDispatching(
"Waiting for message response timed out or did not meet expected conditions",
condition,
5);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB