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
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
29
src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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 |
7
src/main/resources/icons/upload.svg
Normal 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 |
7
src/main/resources/icons/upload_dark.svg
Normal 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 |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
src/test/resources/images/test-image.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |