1.1.0 - Add reverse proxy support(closes #4), fix text copy and selection(fixes #6)

This commit is contained in:
Carl-Robert Linnupuu 2023-02-26 21:51:45 +00:00
parent 841950d153
commit 07c2f6a0d7
31 changed files with 812 additions and 285 deletions

View file

@ -1,116 +0,0 @@
package ee.carlrobert.chatgpt.client;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodySubscriber;
import java.net.http.HttpResponse.ResponseInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import javax.annotation.Nullable;
public final class ApiClient {
private static final List<Map.Entry<String, String>> queries = new ArrayList<>(); // TODO
private static ApiClient instance;
private final ObjectMapper objectMapper = new ObjectMapper();
private final HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build();
private ApiClient() {
}
public static ApiClient getInstance() {
if (instance == null) {
instance = new ApiClient();
}
return instance;
}
public void getCompletionsAsync(String userPrompt, Consumer<String> onMessage, @Nullable Consumer<String> onComplete) {
var prompt = buildCompletePrompt(userPrompt);
this.client.sendAsync(buildHttpRequest(prompt), respInfo -> subscribe(respInfo, userPrompt, onMessage, onComplete));
}
public void clearQueries() {
queries.clear();
}
private String buildCompletePrompt(String prompt) {
var basePrompt = new StringBuilder("""
You are ChatGPT, a large language model trained by OpenAI.
One of your main goals is code generation but not only.
Answer in a markdown language. Markdown code blocks should contain language whenever possible.
""");
queries.forEach(query ->
basePrompt.append("User:\n")
.append(query.getKey())
.append("<|im_end|>\n")
.append("\n")
.append("ChatGPT:\n")
.append(query.getValue())
.append("<|im_end|>\n")
.append("\n"));
basePrompt.append("User:\n")
.append(prompt)
.append("<|im_end|>\n")
.append("\n")
.append("ChatGPT:\n");
return basePrompt.toString();
}
private HttpRequest buildHttpRequest(String prompt) {
try {
return HttpRequest.newBuilder()
.uri(URI.create("https://api.openai.com/v1/completions"))
.header("Accept", "text/event-stream")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + SettingsState.getInstance().secretKey)
.POST(HttpRequest.BodyPublishers.ofString(objectMapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(Map.of(
"model", "text-davinci-003",
"stop", List.of("<|im_end|>"),
"prompt", prompt,
"max_tokens", 1000,
"temperature", 1.0,
"stream", true
))))
.build();
} catch (JsonProcessingException e) {
throw new RuntimeException("Unable to serialize request payload", e);
}
}
private BodySubscriber<Void> subscribe(
ResponseInfo responseInfo,
String userPrompt,
Consumer<String> onMessage,
@Nullable Consumer<String> onComplete) {
if (responseInfo.statusCode() == 200) {
return new Subscriber((messageData ->
onMessage.accept(messageData.choices().get(0).text())),
(finalMsg) -> {
queries.add(Map.entry(userPrompt, finalMsg));
if (onComplete != null) {
onComplete.accept(finalMsg);
}
});
} else if (responseInfo.statusCode() == 401) {
onMessage.accept("Incorrect API key provided.\n" +
"You can find your API key at https://platform.openai.com/account/api-keys.");
throw new IllegalArgumentException();
} else if (responseInfo.statusCode() == 429) {
onMessage.accept("You exceeded your current quota, please check your plan and billing details.");
throw new RuntimeException("Insufficient quota");
} else {
onMessage.accept("Something went wrong. Please try again later.");
clearQueries();
throw new RuntimeException();
}
}
}

View file

@ -0,0 +1,28 @@
package ee.carlrobert.chatgpt.client;
import java.util.Map;
public class ApiRequestDetails {
private final String url;
private final Map<String, Object> body;
private final String token;
public ApiRequestDetails(String url, Map<String, Object> body, String token) {
this.url = url;
this.body = body;
this.token = token;
}
public String getUrl() {
return url;
}
public Map<String, Object> getBody() {
return body;
}
public String getToken() {
return token;
}
}

View file

@ -0,0 +1,4 @@
package ee.carlrobert.chatgpt.client;
public interface ApiResponse {
}

View file

@ -0,0 +1,55 @@
package ee.carlrobert.chatgpt.client;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ee.carlrobert.chatgpt.EmptyCallback;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodySubscriber;
import java.net.http.HttpResponse.ResponseInfo;
import java.util.function.Consumer;
public abstract class Client {
private final HttpClient client =
HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build();
private final ObjectMapper objectMapper = new ObjectMapper();
protected String userPrompt = "";
protected abstract ApiRequestDetails getRequestDetails(String prompt);
public abstract void clearPreviousSession();
protected abstract BodySubscriber<Void> subscribe(
ResponseInfo responseInfo,
Consumer<String> onMessageReceived,
EmptyCallback onComplete);
public void getCompletionsAsync(
String prompt,
Consumer<String> onMessageReceived,
EmptyCallback onComplete) {
this.userPrompt = prompt;
this.client.sendAsync(
buildHttpRequest(prompt),
responseInfo -> subscribe(responseInfo, onMessageReceived, onComplete));
}
private HttpRequest buildHttpRequest(String prompt) {
var requestDetails = getRequestDetails(prompt);
try {
return HttpRequest.newBuilder()
.uri(URI.create(requestDetails.getUrl()))
.header("Accept", "text/event-stream")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + requestDetails.getToken())
.POST(HttpRequest.BodyPublishers.ofString(objectMapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(requestDetails.getBody())))
.build();
} catch (JsonProcessingException e) {
throw new RuntimeException("Unable to serialize request payload", e);
}
}
}

View file

@ -0,0 +1,15 @@
package ee.carlrobert.chatgpt.client;
import ee.carlrobert.chatgpt.client.chatgpt.ChatGPTClient;
import ee.carlrobert.chatgpt.client.gpt.GPTClient;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
public class ClientFactory {
public Client getClient() {
if (SettingsState.getInstance().isGPTOptionSelected) {
return GPTClient.getInstance();
}
return ChatGPTClient.getInstance();
}
}

View file

@ -2,9 +2,8 @@ package ee.carlrobert.chatgpt.client;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.fasterxml.jackson.databind.ObjectMapper;
import ee.carlrobert.chatgpt.client.response.ApiResponse;
import java.net.http.HttpResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.net.http.HttpResponse.BodySubscriber;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@ -13,41 +12,29 @@ import java.util.concurrent.Flow.Subscription;
import java.util.function.Consumer;
import java.util.regex.Pattern;
public class Subscriber implements HttpResponse.BodySubscriber<Void> {
public abstract class Subscriber<T extends ApiResponse> implements BodySubscriber<Void> {
private static final Pattern dataLinePattern = Pattern.compile("^data: ?(.*)$");
protected final CompletableFuture<Void> future = new CompletableFuture<>();
private final Consumer<T> responseConsumer;
private volatile Subscription subscription;
private volatile String deferredText;
private final Consumer<? super ApiResponse> messageDataConsumer;
private final CompletableFuture<Void> future;
private final Consumer<String> onComplete;
private final StringBuilder msgBuilder = new StringBuilder();
public Subscriber(Consumer<? super ApiResponse> messageDataConsumer, Consumer<String> onComplete) {
this.messageDataConsumer = messageDataConsumer;
this.future = new CompletableFuture<>();
this.subscription = null;
this.deferredText = null;
this.onComplete = onComplete;
protected abstract T deserializePayload(String jsonPayload) throws JsonProcessingException;
protected abstract void onRequestComplete();
// Overridden from concrete class
protected void processRegularResponse(String response) {
}
protected static ApiResponse extractMessageData(String[] messageLines) {
var responseBuilder = new StringBuilder();
for (var line : messageLines) {
var matcher = dataLinePattern.matcher(line);
if (matcher.matches()) {
responseBuilder.append(matcher.group(1));
}
}
try {
return new ObjectMapper().readValue(responseBuilder.toString(), ApiResponse.class);
} catch (Exception e) {
throw new RuntimeException("Couldn't read the payload", e);
}
public Subscriber(Consumer<T> responseConsumer) {
this.responseConsumer = responseConsumer;
}
public CompletionStage<Void> getBody() {
return this.future;
}
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
try {
@ -59,24 +46,27 @@ public class Subscriber implements HttpResponse.BodySubscriber<Void> {
}
}
@Override
public void onNext(List<ByteBuffer> buffers) {
try {
var deferredText = this.deferredText;
for (var buffer : buffers) {
var decodedText = deferredText + UTF_8.decode(buffer);
var tokens = decodedText.split("\n\n", -1);
if (tokens.length == 1) {
processRegularResponse(decodedText);
}
for (var i = 0; i < tokens.length - 1; i++) {
var response = extractMessageData(tokens[i].split("\n"));
var choice = response.choices().get(0);
if ("stop".equals(choice.finishReason())) {
onComplete();
var responsePayload = extractPayload(tokens[i].split("\n"));
if ("[DONE]".equals(responsePayload)) {
future.complete(null);
} else {
msgBuilder.append(choice.text());
try {
this.responseConsumer.accept(deserializePayload(responsePayload));
} catch (JsonProcessingException e) {
throw new RuntimeException("Couldn't deserialize the payload", e);
}
}
this.messageDataConsumer.accept(response);
}
deferredText = tokens[tokens.length - 1];
}
@ -89,23 +79,28 @@ public class Subscriber implements HttpResponse.BodySubscriber<Void> {
}
}
@Override
public void onError(Throwable e) {
this.future.completeExceptionally(e);
}
@Override
public void onComplete() {
try {
this.future.complete(null);
this.onComplete.accept(msgBuilder.toString());
onRequestComplete();
} catch (Exception e) {
this.future.completeExceptionally(e);
}
}
@Override
public CompletionStage<Void> getBody() {
return this.future;
private String extractPayload(String[] payload) {
Pattern dataLinePattern = Pattern.compile("^data: ?(.*)$");
var responseBuilder = new StringBuilder();
for (var line : payload) {
var matcher = dataLinePattern.matcher(line);
if (matcher.matches()) {
responseBuilder.append(matcher.group(1));
}
}
return responseBuilder.toString();
}
}

View file

@ -0,0 +1,45 @@
package ee.carlrobert.chatgpt.client.chatgpt;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ee.carlrobert.chatgpt.client.Subscriber;
import ee.carlrobert.chatgpt.client.chatgpt.response.ChatGPTResponse;
import ee.carlrobert.chatgpt.client.chatgpt.response.ChatGPTResponseError;
import java.util.function.Consumer;
public class ChatGPTBodySubscriber extends Subscriber<ChatGPTResponse> {
private final Consumer<ChatGPTResponse> onCompleteCallback;
private final Consumer<ChatGPTResponseError> onErrorCallback;
private final ObjectMapper objectMapper = new ObjectMapper();
private ChatGPTResponse lastReceivedResponse;
public ChatGPTBodySubscriber(
Consumer<ChatGPTResponse> responseConsumer,
Consumer<ChatGPTResponse> onCompleteCallback,
Consumer<ChatGPTResponseError> onErrorCallback) {
super(responseConsumer);
this.onCompleteCallback = onCompleteCallback;
this.onErrorCallback = onErrorCallback;
}
protected ChatGPTResponse deserializePayload(String jsonPayload) throws JsonProcessingException {
lastReceivedResponse = objectMapper.readValue(jsonPayload, ChatGPTResponse.class);
return lastReceivedResponse;
}
protected void onRequestComplete() {
onCompleteCallback.accept(lastReceivedResponse);
}
protected void processRegularResponse(String jsonPayload) {
try {
onErrorCallback.accept(objectMapper.readValue(jsonPayload, ChatGPTResponseError.class));
future.complete(null);
} catch (JsonProcessingException e) {
future.completeExceptionally(e);
}
}
}

View file

@ -0,0 +1,85 @@
package ee.carlrobert.chatgpt.client.chatgpt;
import ee.carlrobert.chatgpt.EmptyCallback;
import ee.carlrobert.chatgpt.client.ApiRequestDetails;
import ee.carlrobert.chatgpt.client.Client;
import ee.carlrobert.chatgpt.client.chatgpt.response.ChatGPTResponse;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
public class ChatGPTClient extends Client {
private static ChatGPTClient instance;
private static ChatGPTResponse lastReceivedResponse;
private ChatGPTClient() {
}
public static ChatGPTClient getInstance() {
if (instance == null) {
instance = new ChatGPTClient();
}
return instance;
}
public void clearPreviousSession() {
lastReceivedResponse = null;
}
protected ApiRequestDetails getRequestDetails(String prompt) {
var settings = SettingsState.getInstance();
var payload = new HashMap<>(Map.of(
"action", "next",
"messages", List.of(Map.of(
"id", UUID.randomUUID().toString(),
"role", "user",
"author", Map.of("role", "user"),
"content", Map.of(
"content_type", "text",
"parts", List.of(prompt)
)
)),
"model", "text-davinci-002-render-sha"
));
if (lastReceivedResponse != null) {
payload.put("conversation_id", lastReceivedResponse.getConversationId());
payload.put("parent_message_id", lastReceivedResponse.getMessage().getId());
} else {
payload.put("parent_message_id", UUID.randomUUID().toString());
}
return new ApiRequestDetails(
settings.reverseProxyUrl,
payload,
settings.accessToken);
}
protected HttpResponse.BodySubscriber<Void> subscribe(
HttpResponse.ResponseInfo responseInfo,
Consumer<String> onMessageReceived,
EmptyCallback onComplete) {
if (responseInfo.statusCode() == 200) {
return new ChatGPTBodySubscriber((
response -> onMessageReceived.accept(String.join("", response.getMessage().getContent().getParts()))),
response -> {
lastReceivedResponse = response;
onComplete.call();
},
error -> {
if ("invalid_api_key".equals(error.getDetail().getCode())) {
onMessageReceived.accept(error.getDetail().getMessage());
}
});
} else {
onMessageReceived.accept("Something went wrong. Please try again later.");
throw new RuntimeException();
}
}
}

View file

@ -0,0 +1,29 @@
package ee.carlrobert.chatgpt.client.chatgpt.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import ee.carlrobert.chatgpt.client.ApiResponse;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatGPTResponse implements ApiResponse {
private ChatGPTResponseMessage message;
@JsonProperty("conversation_id")
private String conversationId;
public ChatGPTResponseMessage getMessage() {
return message;
}
public void setMessage(ChatGPTResponseMessage message) {
this.message = message;
}
public String getConversationId() {
return conversationId;
}
public void setConversationId(String conversationId) {
this.conversationId = conversationId;
}
}

View file

@ -0,0 +1,17 @@
package ee.carlrobert.chatgpt.client.chatgpt.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatGPTResponseError {
private ChatGPTResponseErrorDetails detail;
public ChatGPTResponseErrorDetails getDetail() {
return detail;
}
public void setDetail(ChatGPTResponseErrorDetails detail) {
this.detail = detail;
}
}

View file

@ -0,0 +1,35 @@
package ee.carlrobert.chatgpt.client.chatgpt.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatGPTResponseErrorDetails {
private String message;
private String type;
private String code;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}

View file

@ -0,0 +1,26 @@
package ee.carlrobert.chatgpt.client.chatgpt.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatGPTResponseMessage {
private String id;
private ChatGPTResponseMessageContent content;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public ChatGPTResponseMessageContent getContent() {
return content;
}
public void setContent(ChatGPTResponseMessageContent content) {
this.content = content;
}
}

View file

@ -0,0 +1,29 @@
package ee.carlrobert.chatgpt.client.chatgpt.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatGPTResponseMessageContent {
@JsonProperty("content_type")
private String contentType;
private List<String> parts;
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public List<String> getParts() {
return parts;
}
public void setParts(List<String> parts) {
this.parts = parts;
}
}

View file

@ -0,0 +1,30 @@
package ee.carlrobert.chatgpt.client.gpt;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ee.carlrobert.chatgpt.client.Subscriber;
import ee.carlrobert.chatgpt.client.gpt.response.GPTResponse;
import java.util.function.Consumer;
public class GPTBodySubscriber extends Subscriber<GPTResponse> {
private final Consumer<String> onCompleteCallback;
private final StringBuilder messageBuilder = new StringBuilder();
public GPTBodySubscriber(
Consumer<GPTResponse> responseConsumer,
Consumer<String> onCompleteCallback) {
super(responseConsumer);
this.onCompleteCallback = onCompleteCallback;
}
protected GPTResponse deserializePayload(String jsonPayload) throws JsonProcessingException {
var response = new ObjectMapper().readValue(jsonPayload, GPTResponse.class);
messageBuilder.append(response.getChoices().get(0).getText());
return response;
}
protected void onRequestComplete() {
onCompleteCallback.accept(messageBuilder.toString());
}
}

View file

@ -0,0 +1,93 @@
package ee.carlrobert.chatgpt.client.gpt;
import ee.carlrobert.chatgpt.EmptyCallback;
import ee.carlrobert.chatgpt.client.ApiRequestDetails;
import ee.carlrobert.chatgpt.client.Client;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodySubscriber;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
public class GPTClient extends Client {
private static final List<Map.Entry<String, String>> queries = new ArrayList<>();
private static GPTClient instance;
private GPTClient() {
}
public static GPTClient getInstance() {
if (instance == null) {
instance = new GPTClient();
}
return instance;
}
public void clearPreviousSession() {
queries.clear();
}
protected ApiRequestDetails getRequestDetails(String prompt) {
return new ApiRequestDetails(
"https://api.openai.com/v1/completions",
Map.of(
"model", "text-davinci-003",
"stop", List.of("<|im_end|>"),
"prompt", buildPrompt(prompt),
"max_tokens", 1000,
"temperature", 1.0,
"stream", true
),
SettingsState.getInstance().apiKey);
}
protected BodySubscriber<Void> subscribe(
HttpResponse.ResponseInfo responseInfo,
Consumer<String> onMessageReceived,
EmptyCallback onComplete) {
if (responseInfo.statusCode() == 200) {
return new GPTBodySubscriber(
response -> onMessageReceived.accept(response.getChoices().get(0).getText()),
finalMsg -> {
queries.add(Map.entry(super.userPrompt, finalMsg));
onComplete.call();
});
} else if (responseInfo.statusCode() == 401) {
onMessageReceived.accept("Incorrect API key provided.\n" +
"You can find your API key at https://platform.openai.com/account/api-keys.");
throw new IllegalArgumentException();
} else if (responseInfo.statusCode() == 429) {
onMessageReceived.accept("You exceeded your current quota, please check your plan and billing details.");
throw new RuntimeException("Insufficient quota");
} else {
onMessageReceived.accept("Something went wrong. Please try again later.");
throw new RuntimeException();
}
}
private String buildPrompt(String prompt) {
var basePrompt = new StringBuilder("""
You are ChatGPT, a large language model trained by OpenAI.
One of your main goals is code generation but not only.
Answer in a markdown language. Markdown code blocks should contain language whenever possible.
""");
queries.forEach(query ->
basePrompt.append("User:\n")
.append(query.getKey())
.append("<|im_end|>\n")
.append("\n")
.append("ChatGPT:\n")
.append(query.getValue())
.append("<|im_end|>\n")
.append("\n"));
basePrompt.append("User:\n")
.append(prompt)
.append("<|im_end|>\n")
.append("\n")
.append("ChatGPT:\n");
return basePrompt.toString();
}
}

View file

@ -0,0 +1,28 @@
package ee.carlrobert.chatgpt.client.gpt.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import ee.carlrobert.chatgpt.client.ApiResponse;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class GPTResponse implements ApiResponse {
private String id;
private List<GPTResponseChoice> choices;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<GPTResponseChoice> getChoices() {
return choices;
}
public void setChoices(List<GPTResponseChoice> choices) {
this.choices = choices;
}
}

View file

@ -0,0 +1,28 @@
package ee.carlrobert.chatgpt.client.gpt.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class GPTResponseChoice {
private String text;
@JsonProperty("finish_reason")
private String finishReason;
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getFinishReason() {
return finishReason;
}
public void setFinishReason(String finishReason) {
this.finishReason = finishReason;
}
}

View file

@ -1,6 +0,0 @@
package ee.carlrobert.chatgpt.client.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public record ApiError(String message, String type) {}

View file

@ -1,12 +0,0 @@
package ee.carlrobert.chatgpt.client.response;
import java.util.List;
public record ApiResponse(
String id,
String object,
long created,
String model,
List<ApiResponseChoice> choices,
ApiResponseUsage usage,
ApiError error) {}

View file

@ -1,9 +0,0 @@
package ee.carlrobert.chatgpt.client.response;
import com.fasterxml.jackson.annotation.JsonProperty;
public record ApiResponseChoice(
String text,
int index,
Object logprobs,
@JsonProperty("finish_reason") String finishReason) {}

View file

@ -1,8 +0,0 @@
package ee.carlrobert.chatgpt.client.response;
import com.fasterxml.jackson.annotation.JsonProperty;
public record ApiResponseUsage(
@JsonProperty("prompt_tokens") int promptTokens,
@JsonProperty("completion_tokens") int completionTokens,
@JsonProperty("total_tokens") int totalTokens) {}

View file

@ -17,7 +17,7 @@ public class ActionGroup extends DefaultActionGroup {
Project project = event.getProject();
boolean menuAllowed = false;
if (editor != null && project != null) {
var secretKey = SettingsState.getInstance().secretKey;
var secretKey = SettingsState.getInstance().apiKey;
menuAllowed = secretKey != null && !secretKey.isEmpty() && editor.getSelectionModel().getSelectedText() != null;
}
event.getPresentation().setEnabled(menuAllowed);

View file

@ -3,7 +3,7 @@ package ee.carlrobert.chatgpt.ide.action;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import ee.carlrobert.chatgpt.client.ApiClient;
import ee.carlrobert.chatgpt.client.ClientFactory;
import ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowService;
import org.jetbrains.annotations.NotNull;
@ -18,13 +18,13 @@ public class AskAction extends AnAction {
public void actionPerformed(@NotNull AnActionEvent event) {
var project = event.getProject();
if (project != null) {
var toolWindowService = ApplicationManager.getApplication().getService(ToolWindowService.class);
var toolWindow = toolWindowService.getToolWindow(project);
ApiClient.getInstance().clearQueries();
toolWindow.show();
toolWindow.setTitle("");
toolWindowService.removeAll();
toolWindowService.paintLandingView();
var toolWindowService = ApplicationManager.getApplication().getService(ToolWindowService.class);
var toolWindow = toolWindowService.getToolWindow(project);
new ClientFactory().getClient().clearPreviousSession();
toolWindow.show();
toolWindow.setTitle("");
toolWindowService.removeAll();
toolWindowService.paintLandingView();
}
}
}

View file

@ -5,7 +5,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.wm.ToolWindow;
import ee.carlrobert.chatgpt.client.ApiClient;
import ee.carlrobert.chatgpt.client.ClientFactory;
import ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowService;
import org.jetbrains.annotations.NotNull;
@ -21,7 +21,7 @@ public abstract class BaseAction extends AnAction {
if (editor != null && project != null) {
var toolWindowService = ApplicationManager.getApplication().getService(ToolWindowService.class);
var selectedText = editor.getSelectionModel().getSelectedText();
ApiClient.getInstance().clearQueries();
new ClientFactory().getClient().clearPreviousSession();
initToolWindow(toolWindowService.getToolWindow(project));
toolWindowService.removeAll();
toolWindowService.paintUserMessage(selectedText);

View file

@ -11,8 +11,8 @@ public class ShowNotificationActivity implements StartupActivity {
@Override
public void runActivity(@NotNull Project project) {
var notificationService = ApplicationManager.getApplication().getService(NotificationService.class);
var secretKey = SettingsState.getInstance().secretKey;
if (secretKey == null || secretKey.isEmpty()) {
var apiKey = SettingsState.getInstance().apiKey;
if (apiKey == null || apiKey.isEmpty()) {
notificationService.createAndNotify(project);
}
}

View file

@ -1,44 +1,60 @@
package ee.carlrobert.chatgpt.ide.settings;
import com.intellij.ui.TitledSeparator;
import com.intellij.ui.components.JBRadioButton;
import com.intellij.ui.components.JBTextField;
import com.intellij.util.ui.FormBuilder;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UI;
import java.awt.Desktop;
import java.io.IOException;
import java.net.URISyntaxException;
import javax.swing.ButtonGroup;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.event.HyperlinkEvent;
import org.jetbrains.annotations.NotNull;
public class SettingsComponent {
private final JPanel myMainPanel;
private final JBTextField apiKeyField = new JBTextField();
private final JPanel mainPanel;
private final JBTextField apiKeyField;
private final JComboBox<String> reverseProxyComboBox;
private final JBTextField accessTokenField;
private final JBRadioButton useGPTRadioButton;
private final JBRadioButton useChatGPTRadioButton;
public SettingsComponent() {
myMainPanel = FormBuilder.createFormBuilder()
.addComponent(UI.PanelFactory.panel(apiKeyField)
.withLabel("API key:")
.withComment("You can find your Secret API key in your <a href=\"https://platform.openai.com/account/api-keys\">User settings</a>.")
.withCommentHyperlinkListener(event -> {
if (HyperlinkEvent.EventType.ACTIVATED.equals(event.getEventType())) {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
try {
Desktop.getDesktop().browse(event.getURL().toURI());
} catch (IOException | URISyntaxException e) {
throw new RuntimeException("Couldn't open the browser.", e);
}
}
}
})
.createPanel())
public SettingsComponent(SettingsState settings) {
apiKeyField = new JBTextField(settings.apiKey);
reverseProxyComboBox = new JComboBox<>(new String[] {
"https://chat.duti.tech/api/conversation",
"https://gpt.pawan.krd/backend-api/conversation"
});
accessTokenField = new JBTextField(settings.accessToken, 1);
useGPTRadioButton = new JBRadioButton("Use OpenAI's official GPT3 API", settings.isGPTOptionSelected);
useChatGPTRadioButton = new JBRadioButton("Use ChatGPT's unofficial backend API", settings.isChatGPTOptionSelected);
mainPanel = FormBuilder.createFormBuilder()
.addComponent(new TitledSeparator("Integration Preference"))
.addComponent(createMainSelectionForm())
.addComponentFillVertically(new JPanel(), 0)
.getPanel();
if (settings.isGPTOptionSelected) {
reverseProxyComboBox.setEnabled(false);
accessTokenField.setEnabled(false);
} else {
apiKeyField.setEnabled(false);
reverseProxyComboBox.setEnabled(true);
accessTokenField.setEnabled(true);
}
registerButtons();
}
public JPanel getPanel() {
return myMainPanel;
return mainPanel;
}
public JComponent getPreferredFocusedComponent() {
@ -46,11 +62,126 @@ public class SettingsComponent {
}
@NotNull
public String getApiKeyField() {
public String getApiKey() {
return apiKeyField.getText();
}
public void setApiKeyField(@NotNull String apiKey) {
public void setApiKey(@NotNull String apiKey) {
apiKeyField.setText(apiKey);
}
@NotNull
public String getAccessToken() {
return accessTokenField.getText();
}
public void setAccessToken(@NotNull String accessToken) {
accessTokenField.setText(accessToken);
}
public boolean isGPTOptionSelected() {
return useGPTRadioButton.isSelected();
}
public void setUseGPTOptionSelected(boolean isSelected) {
useGPTRadioButton.setSelected(isSelected);
}
public boolean isChatGPTOptionSelected() {
return useChatGPTRadioButton.isSelected();
}
public void setUseChatGPTOptionSelected(boolean isSelected) {
useChatGPTRadioButton.setSelected(isSelected);
}
public String getReverseProxyUrl() {
return (String) reverseProxyComboBox.getSelectedItem();
}
public void setReverseProxyUrl(String reverseProxyUrl) {
reverseProxyComboBox.setSelectedItem(reverseProxyUrl);
}
private JPanel createMainSelectionForm() {
var panel = FormBuilder.createFormBuilder()
.addVerticalGap(8)
.addComponent(UI.PanelFactory.panel(useGPTRadioButton)
.withComment("Fast and robust, requires API key")
.createPanel())
.addComponent(createFirstSelectionForm())
.addVerticalGap(8)
.addComponent(UI.PanelFactory.panel(useChatGPTRadioButton)
.withComment("Slow and free, more suitable for conversational tasks")
.createPanel())
.addComponent(createSecondSelectionForm())
.getPanel();
panel.setBorder(JBUI.Borders.emptyLeft(16));
return panel;
}
private JPanel createFirstSelectionForm() {
var panel = FormBuilder.createFormBuilder()
.addComponent(UI.PanelFactory.panel(apiKeyField)
.withLabel("API key:")
.withComment("You can find your Secret API key in your <a href=\"https://platform.openai.com/account/api-keys\">User settings</a>.")
.withCommentHyperlinkListener(this::handleHyperlinkClicked)
.createPanel())
.getPanel();
panel.setBorder(JBUI.Borders.emptyLeft(24));
return panel;
}
private JPanel createSecondSelectionForm() {
var reverseProxyUrlPanel = FormBuilder.createFormBuilder()
.addLabeledComponent("Reverse proxy url:", reverseProxyComboBox)
.getPanel();
var accessTokenPanel = UI.PanelFactory.panel(accessTokenField)
.withLabel("Access token:")
.withComment(
"Access token can be obtained from <a href=\"https://chat.openai.com/api/auth/session\">https://chat.openai.com/api/auth/session</a> and is valid for ~8h.")
.withCommentHyperlinkListener(this::handleHyperlinkClicked)
.createPanel();
var accessTokenLabel = accessTokenPanel.getComponents()[0];
var reverseProxyUrlLabel = reverseProxyUrlPanel.getComponents()[0];
if (accessTokenLabel instanceof JLabel && reverseProxyUrlLabel instanceof JLabel) {
accessTokenLabel.setPreferredSize(reverseProxyUrlLabel.getPreferredSize());
}
var panel = FormBuilder.createFormBuilder()
.addComponent(reverseProxyUrlPanel)
.addVerticalGap(8)
.addComponent(accessTokenPanel)
.getPanel();
panel.setBorder(JBUI.Borders.emptyLeft(24));
return panel;
}
private void registerButtons() {
ButtonGroup myButtonGroup = new ButtonGroup();
myButtonGroup.add(useGPTRadioButton);
myButtonGroup.add(useChatGPTRadioButton);
useGPTRadioButton.addActionListener(e -> handleRadioOptionChange(false));
useChatGPTRadioButton.addActionListener(e -> handleRadioOptionChange(true));
}
private void handleRadioOptionChange(boolean isUseChatGPTOption) {
apiKeyField.setEnabled(!isUseChatGPTOption);
accessTokenField.setEnabled(isUseChatGPTOption);
reverseProxyComboBox.setEnabled(isUseChatGPTOption);
}
private void handleHyperlinkClicked(HyperlinkEvent event) {
if (HyperlinkEvent.EventType.ACTIVATED.equals(event.getEventType())) {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
try {
Desktop.getDesktop().browse(event.getURL().toURI());
} catch (IOException | URISyntaxException e) {
throw new RuntimeException("Couldn't open the browser.", e);
}
}
}
}
}

View file

@ -1,8 +1,6 @@
package ee.carlrobert.chatgpt.ide.settings;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.options.Configurable;
import ee.carlrobert.chatgpt.ide.notification.NotificationService;
import javax.swing.JComponent;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.Nullable;
@ -25,29 +23,39 @@ public class SettingsConfigurable implements Configurable {
@Nullable
@Override
public JComponent createComponent() {
settingsComponent = new SettingsComponent();
var settings = SettingsState.getInstance();
settingsComponent = new SettingsComponent(settings);
return settingsComponent.getPanel();
}
@Override
public boolean isModified() {
var settings = SettingsState.getInstance();
return !settingsComponent.getApiKeyField().equals(settings.secretKey);
return !settingsComponent.getApiKey().equals(settings.apiKey) ||
!settingsComponent.getAccessToken().equals(settings.accessToken) ||
!settingsComponent.getReverseProxyUrl().equals(settings.reverseProxyUrl) ||
settingsComponent.isGPTOptionSelected() != settings.isGPTOptionSelected ||
settingsComponent.isChatGPTOptionSelected() != settings.isChatGPTOptionSelected;
}
@Override
public void apply() {
var settings = SettingsState.getInstance();
settings.secretKey = settingsComponent.getApiKeyField();
if (!settings.secretKey.isEmpty()) {
ApplicationManager.getApplication().getService(NotificationService.class).expire();
}
settings.isGPTOptionSelected = settingsComponent.isGPTOptionSelected();
settings.isChatGPTOptionSelected = settingsComponent.isChatGPTOptionSelected();
settings.accessToken = settingsComponent.getAccessToken();
settings.apiKey = settingsComponent.getApiKey();
settings.reverseProxyUrl = settingsComponent.getReverseProxyUrl();
}
@Override
public void reset() {
var settings = SettingsState.getInstance();
settingsComponent.setApiKeyField(settings.secretKey);
settingsComponent.setUseGPTOptionSelected(settings.isGPTOptionSelected);
settingsComponent.setUseChatGPTOptionSelected(settings.isChatGPTOptionSelected);
settingsComponent.setAccessToken(settings.accessToken);
settingsComponent.setApiKey(settings.apiKey);
settingsComponent.setReverseProxyUrl(settings.reverseProxyUrl);
}
@Override

View file

@ -14,7 +14,11 @@ import org.jetbrains.annotations.Nullable;
)
public class SettingsState implements PersistentStateComponent<SettingsState> {
public String secretKey = "";
public String apiKey = "";
public String accessToken = "";
public String reverseProxyUrl = "";
public boolean isGPTOptionSelected = true;
public boolean isChatGPTOptionSelected = false;
public static SettingsState getInstance() {
return ApplicationManager.getApplication().getService(SettingsState.class);

View file

@ -12,7 +12,8 @@ import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowManager;
import ee.carlrobert.chatgpt.EmptyCallback;
import ee.carlrobert.chatgpt.client.ApiClient;
import ee.carlrobert.chatgpt.client.ClientFactory;
import ee.carlrobert.chatgpt.client.chatgpt.ChatGPTClient;
import ee.carlrobert.chatgpt.ide.settings.SettingsConfigurable;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
import ee.carlrobert.chatgpt.ide.toolwindow.components.SyntaxTextArea;
@ -23,7 +24,6 @@ import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import javax.swing.Box;
import javax.swing.ImageIcon;
@ -60,8 +60,8 @@ public class ToolWindowService implements LafManagerListener {
addIconLabel(Icons.DefaultImageIcon, "ChatGPT:");
addSpacing(8);
var secretKey = SettingsState.getInstance().secretKey;
if (secretKey == null || secretKey.isEmpty()) {
var apiKey = SettingsState.getInstance().apiKey;
if (apiKey == null || apiKey.isEmpty()) {
var label = new JLabel("<html>API key not provided. <font color='#589df6'><u>Open Settings</u></font> to set one.</html>");
label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
label.addMouseListener(new MouseAdapter() {
@ -75,19 +75,21 @@ public class ToolWindowService implements LafManagerListener {
scrollablePanel.add(textArea);
textAreas.add(textArea);
var messageCounter = new AtomicInteger(0);
ApiClient.getInstance().getCompletionsAsync(
prompt,
(message) -> {
if (messageCounter.getAndIncrement() == 0) {
message = message.replace("\n", "");
}
textArea.append(message);
if (scrollToBottom != null) {
scrollToBottom.call();
}
},
(finalMessage) -> textArea.displayCopyButton());
var client = new ClientFactory().getClient();
client.getCompletionsAsync(prompt, message -> {
if (client instanceof ChatGPTClient) {
textArea.setText(message);
} else {
textArea.append(message);
}
if (scrollToBottom != null) {
scrollToBottom.call();
}
}, () -> {
textArea.displayCopyButton();
textArea.enableSelection();
});
}
addSpacing(16);

View file

@ -9,8 +9,6 @@ import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JButton;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
@ -23,15 +21,25 @@ public class SyntaxTextArea extends RSyntaxTextArea {
setStyles();
}
public Matcher getMarkdownMatcher() {
return Pattern.compile("`{3}([\\w]*)\\n([\\S\\s]+?)\\n`{3}").matcher(getText());
public void displayCopyButton() {
ComponentBorder cb = new ComponentBorder(createCopyButton());
cb.setAlignment(TOP_ALIGNMENT);
cb.install(this);
}
public void displayCopyButton() {
if (getMarkdownMatcher().matches()) {
ComponentBorder cb = new ComponentBorder(createCopyButton());
cb.setAlignment(TOP_ALIGNMENT);
cb.install(this);
public void enableSelection() {
setEditable(false);
setEnabled(true);
}
public void changeStyleViaThemeXml() {
var baseThemePath = "/org/fife/ui/rsyntaxtextarea/themes/";
try {
Theme theme = Theme.load(getClass().getResourceAsStream(
UIUtil.isUnderDarcula() ? baseThemePath + "dark.xml" : baseThemePath + "idea.xml"));
theme.apply(this);
} catch (IOException e) {
e.printStackTrace();
}
}
@ -48,28 +56,11 @@ public class SyntaxTextArea extends RSyntaxTextArea {
}
private void copyToClipboard() {
var text = getText();
var matcher = getMarkdownMatcher();
if (matcher.find()) {
text = matcher.group(2);
}
StringSelection stringSelection = new StringSelection(text);
StringSelection stringSelection = new StringSelection(getText());
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(stringSelection, null);
}
public void changeStyleViaThemeXml() {
var baseThemePath = "/org/fife/ui/rsyntaxtextarea/themes/";
try {
Theme theme = Theme.load(getClass().getResourceAsStream(
UIUtil.isUnderDarcula() ? baseThemePath + "dark.xml" : baseThemePath + "idea.xml"));
theme.apply(this);
} catch (IOException e) {
e.printStackTrace();
}
}
private JButton createCopyButton() {
var button = createIconButton(Icons.CopyImageIcon);
button.addActionListener(e -> {