mirror of
https://github.com/theonedev/onedev.git
synced 2025-12-10 10:12:19 -06:00
wip: Add page tools to AI chatter
This commit is contained in:
parent
b3d207a835
commit
8554de9e13
2
pom.xml
2
pom.xml
@ -629,7 +629,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-core</artifactId>
|
||||
<artifactId>langchain4j</artifactId>
|
||||
<version>${langchain4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
||||
@ -385,7 +385,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-core</artifactId>
|
||||
<artifactId>langchain4j</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
package io.onedev.server.ai;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.model.Issue;
|
||||
import io.onedev.server.model.IssueComment;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.web.UrlService;
|
||||
|
||||
public class IssueHelper {
|
||||
|
||||
private static ObjectMapper getObjectMapper() {
|
||||
return OneDev.getInstance(ObjectMapper.class);
|
||||
}
|
||||
|
||||
private static UrlService getUrlService() {
|
||||
return OneDev.getInstance(UrlService.class);
|
||||
}
|
||||
|
||||
public static Map<String, Object> getSummary(Project currentProject, Issue issue) {
|
||||
var typeReference = new TypeReference<LinkedHashMap<String, Object>>() {};
|
||||
var summary = getObjectMapper().convertValue(issue, typeReference);
|
||||
summary.remove("id");
|
||||
summary.remove("stateOrdinal");
|
||||
summary.remove("uuid");
|
||||
summary.remove("messageId");
|
||||
summary.remove("pinDate");
|
||||
summary.remove("boardPosition");
|
||||
summary.remove("numberScopeId");
|
||||
summary.put("reference", issue.getReference().toString(currentProject));
|
||||
summary.remove("submitterId");
|
||||
summary.put("submitter", issue.getSubmitter().getName());
|
||||
summary.put("Project", issue.getProject().getPath());
|
||||
summary.remove("lastActivity");
|
||||
for (var it = summary.entrySet().iterator(); it.hasNext();) {
|
||||
var entry = it.next();
|
||||
if (entry.getKey().endsWith("Count"))
|
||||
it.remove();
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
public static List<Map<String, Object>> getComments(Issue issue) {
|
||||
var comments = new ArrayList<Map<String, Object>>();
|
||||
issue.getComments().stream().sorted(Comparator.comparing(IssueComment::getId)).forEach(comment -> {
|
||||
var commentMap = new HashMap<String, Object>();
|
||||
commentMap.put("user", comment.getUser().getName());
|
||||
commentMap.put("date", comment.getDate());
|
||||
commentMap.put("content", comment.getContent());
|
||||
comments.add(commentMap);
|
||||
});
|
||||
return comments;
|
||||
}
|
||||
|
||||
public static Map<String, Object> getDetail(Project currentProject, Issue issue) {
|
||||
var detail = getSummary(currentProject, issue);
|
||||
for (var entry : issue.getFieldInputs().entrySet()) {
|
||||
detail.put(entry.getKey(), entry.getValue().getValues());
|
||||
}
|
||||
|
||||
Map<String, Collection<String>> linkedIssues = new HashMap<>();
|
||||
for (var link: issue.getTargetLinks()) {
|
||||
linkedIssues.computeIfAbsent(link.getSpec().getName(), k -> new ArrayList<>())
|
||||
.add(link.getTarget().getReference().toString(currentProject));
|
||||
}
|
||||
for (var link : issue.getSourceLinks()) {
|
||||
if (link.getSpec().getOpposite() != null) {
|
||||
linkedIssues.computeIfAbsent(link.getSpec().getOpposite().getName(), k -> new ArrayList<>())
|
||||
.add(link.getSource().getReference().toString(currentProject));
|
||||
} else {
|
||||
linkedIssues.computeIfAbsent(link.getSpec().getName(), k -> new ArrayList<>())
|
||||
.add(link.getSource().getReference().toString(currentProject));
|
||||
}
|
||||
}
|
||||
detail.putAll(linkedIssues);
|
||||
detail.put("link", getUrlService().urlFor(issue, true));
|
||||
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,6 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@ -709,16 +708,16 @@ public class McpHelperResource {
|
||||
parsedQuery = new IssueQuery();
|
||||
}
|
||||
|
||||
var issues = new ArrayList<Map<String, Object>>();
|
||||
var summaries = new ArrayList<Map<String, Object>>();
|
||||
for (var issue : issueService.query(subject, new ProjectScope(projectInfo.project, true, false), parsedQuery, true, offset, count)) {
|
||||
var issueMap = getIssueMap(projectInfo.currentProject, issue);
|
||||
var summary = IssueHelper.getSummary(projectInfo.currentProject, issue);
|
||||
for (var entry: issue.getFieldInputs().entrySet()) {
|
||||
issueMap.put(entry.getKey(), entry.getValue().getValues());
|
||||
summary.put(entry.getKey(), entry.getValue().getValues());
|
||||
}
|
||||
issueMap.put("link", urlService.urlFor(issue, true));
|
||||
issues.add(issueMap);
|
||||
summary.put("link", urlService.urlFor(issue, true));
|
||||
summaries.add(summary);
|
||||
}
|
||||
return issues;
|
||||
return summaries;
|
||||
}
|
||||
|
||||
private Issue getIssue(Project currentProject, String referenceString) {
|
||||
@ -733,29 +732,6 @@ public class McpHelperResource {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> getIssueMap(Project currentProject, Issue issue) {
|
||||
var typeReference = new TypeReference<LinkedHashMap<String, Object>>() {};
|
||||
var issueMap = objectMapper.convertValue(issue, typeReference);
|
||||
issueMap.remove("id");
|
||||
issueMap.remove("stateOrdinal");
|
||||
issueMap.remove("uuid");
|
||||
issueMap.remove("messageId");
|
||||
issueMap.remove("pinDate");
|
||||
issueMap.remove("boardPosition");
|
||||
issueMap.remove("numberScopeId");
|
||||
issueMap.put("reference", issue.getReference().toString(currentProject));
|
||||
issueMap.remove("submitterId");
|
||||
issueMap.put("submitter", issue.getSubmitter().getName());
|
||||
issueMap.put("Project", issue.getProject().getPath());
|
||||
issueMap.remove("lastActivity");
|
||||
for (var it = issueMap.entrySet().iterator(); it.hasNext();) {
|
||||
var entry = it.next();
|
||||
if (entry.getKey().endsWith("Count"))
|
||||
it.remove();
|
||||
}
|
||||
return issueMap;
|
||||
}
|
||||
|
||||
@Path("/get-issue")
|
||||
@GET
|
||||
public Map<String, Object> getIssue(
|
||||
@ -766,30 +742,7 @@ public class McpHelperResource {
|
||||
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
var issue = getIssue(currentProject, issueReference);
|
||||
|
||||
var issueMap = getIssueMap(currentProject, issue);
|
||||
for (var entry : issue.getFieldInputs().entrySet()) {
|
||||
issueMap.put(entry.getKey(), entry.getValue().getValues());
|
||||
}
|
||||
|
||||
Map<String, Collection<String>> linkedIssues = new HashMap<>();
|
||||
for (var link: issue.getTargetLinks()) {
|
||||
linkedIssues.computeIfAbsent(link.getSpec().getName(), k -> new ArrayList<>())
|
||||
.add(link.getTarget().getReference().toString(currentProject));
|
||||
}
|
||||
for (var link : issue.getSourceLinks()) {
|
||||
if (link.getSpec().getOpposite() != null) {
|
||||
linkedIssues.computeIfAbsent(link.getSpec().getOpposite().getName(), k -> new ArrayList<>())
|
||||
.add(link.getSource().getReference().toString(currentProject));
|
||||
} else {
|
||||
linkedIssues.computeIfAbsent(link.getSpec().getName(), k -> new ArrayList<>())
|
||||
.add(link.getSource().getReference().toString(currentProject));
|
||||
}
|
||||
}
|
||||
issueMap.putAll(linkedIssues);
|
||||
issueMap.put("link", urlService.urlFor(issue, true));
|
||||
|
||||
return issueMap;
|
||||
return IssueHelper.getDetail(currentProject, issue);
|
||||
}
|
||||
|
||||
@Path("/get-issue-comments")
|
||||
@ -799,20 +752,9 @@ public class McpHelperResource {
|
||||
@QueryParam("reference") @NotNull String issueReference) {
|
||||
if (SecurityUtils.getUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
|
||||
var issue = getIssue(currentProject, issueReference);
|
||||
|
||||
var comments = new ArrayList<Map<String, Object>>();
|
||||
for (var comment : issue.getComments()) {
|
||||
var commentMap = new HashMap<String, Object>();
|
||||
commentMap.put("user", comment.getUser().getName());
|
||||
commentMap.put("date", comment.getDate());
|
||||
commentMap.put("content", comment.getContent());
|
||||
comments.add(commentMap);
|
||||
}
|
||||
return comments;
|
||||
return IssueHelper.getComments(issue);
|
||||
}
|
||||
|
||||
@Path("/add-issue-comment")
|
||||
@ -1093,40 +1035,6 @@ public class McpHelperResource {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> getPullRequestMap(Project currentProject,
|
||||
PullRequest pullRequest, boolean checkMergeConditionIfOpen) {
|
||||
var typeReference = new TypeReference<LinkedHashMap<String, Object>>() {};
|
||||
var pullRequestMap = objectMapper.convertValue(pullRequest, typeReference);
|
||||
pullRequestMap.remove("id");
|
||||
if (pullRequest.isOpen() && checkMergeConditionIfOpen) {
|
||||
var errorMessage = pullRequest.checkMergeCondition();
|
||||
if (errorMessage != null)
|
||||
pullRequestMap.put("status", PullRequest.Status.OPEN.name() + " (" + errorMessage + ")");
|
||||
else
|
||||
pullRequestMap.put("status", PullRequest.Status.OPEN.name() + " (ready to merge)");
|
||||
}
|
||||
pullRequestMap.remove("uuid");
|
||||
pullRequestMap.remove("buildCommitHash");
|
||||
pullRequestMap.remove("submitTimeGroups");
|
||||
pullRequestMap.remove("closeTimeGroups");
|
||||
pullRequestMap.remove("checkError");
|
||||
pullRequestMap.remove("numberScopeId");
|
||||
pullRequestMap.put("reference", pullRequest.getReference().toString(currentProject));
|
||||
pullRequestMap.remove("submitterId");
|
||||
pullRequestMap.put("submitter", pullRequest.getSubmitter().getName());
|
||||
pullRequestMap.put("targetProject", pullRequest.getTarget().getProject().getPath());
|
||||
if (pullRequest.getSourceProject() != null)
|
||||
pullRequestMap.put("sourceProject", pullRequest.getSourceProject().getPath());
|
||||
pullRequestMap.remove("codeCommentsUpdateDate");
|
||||
pullRequestMap.remove("lastActivity");
|
||||
for (var it = pullRequestMap.entrySet().iterator(); it.hasNext();) {
|
||||
var entry = it.next();
|
||||
if (entry.getKey().endsWith("Count"))
|
||||
it.remove();
|
||||
}
|
||||
return pullRequestMap;
|
||||
}
|
||||
|
||||
@Path("/query-pull-requests")
|
||||
@GET
|
||||
public List<Map<String, Object>> queryPullRequests(
|
||||
@ -1154,13 +1062,13 @@ public class McpHelperResource {
|
||||
parsedQuery = new PullRequestQuery();
|
||||
}
|
||||
|
||||
var pullRequests = new ArrayList<Map<String, Object>>();
|
||||
var summaries = new ArrayList<Map<String, Object>>();
|
||||
for (var pullRequest : pullRequestService.query(subject, projectInfo.project, parsedQuery, false, offset, count)) {
|
||||
var pullRequestMap = getPullRequestMap(projectInfo.currentProject, pullRequest, false);
|
||||
pullRequestMap.put("link", urlService.urlFor(pullRequest, true));
|
||||
pullRequests.add(pullRequestMap);
|
||||
var summary = PullRequestHelper.getSummary(projectInfo.currentProject, pullRequest, false);
|
||||
summary.put("link", urlService.urlFor(pullRequest, true));
|
||||
summaries.add(summary);
|
||||
}
|
||||
return pullRequests;
|
||||
return summaries;
|
||||
}
|
||||
|
||||
@Path("/query-builds")
|
||||
@ -1249,31 +1157,8 @@ public class McpHelperResource {
|
||||
throw new UnauthenticatedException();
|
||||
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
|
||||
var pullRequest = getPullRequest(currentProject, pullRequestReference);
|
||||
|
||||
var pullRequestMap = getPullRequestMap(currentProject, pullRequest, true);
|
||||
pullRequestMap.put("headCommitHash", pullRequest.getLatestUpdate().getHeadCommitHash());
|
||||
pullRequestMap.put("assignees", pullRequest.getAssignees().stream().map(it->it.getName()).collect(Collectors.toList()));
|
||||
var reviews = new ArrayList<Map<String, Object>>();
|
||||
for (var review : pullRequest.getReviews()) {
|
||||
if (review.getStatus() == PullRequestReview.Status.EXCLUDED)
|
||||
continue;
|
||||
var reviewMap = new HashMap<String, Object>();
|
||||
reviewMap.put("reviewer", review.getUser().getName());
|
||||
reviewMap.put("status", review.getStatus());
|
||||
reviews.add(reviewMap);
|
||||
}
|
||||
pullRequestMap.put("reviews", reviews);
|
||||
var builds = new ArrayList<String>();
|
||||
for (var build : pullRequest.getBuilds()) {
|
||||
builds.add(build.getReference().toString(currentProject) + " (job: " + build.getJobName() + ", status: " + build.getStatus() + ")");
|
||||
}
|
||||
pullRequestMap.put("builds", builds);
|
||||
pullRequestMap.put("labels", pullRequest.getLabels().stream().map(it->it.getSpec().getName()).collect(Collectors.toList()));
|
||||
pullRequestMap.put("link", urlService.urlFor(pullRequest, true));
|
||||
|
||||
return pullRequestMap;
|
||||
return PullRequestHelper.getDetail(currentProject, pullRequest);
|
||||
}
|
||||
|
||||
@Path("/get-pull-request-comments")
|
||||
@ -1287,15 +1172,7 @@ public class McpHelperResource {
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
var pullRequest = getPullRequest(currentProject, pullRequestReference);
|
||||
|
||||
var comments = new ArrayList<Map<String, Object>>();
|
||||
for (var comment : pullRequest.getComments()) {
|
||||
var commentMap = new HashMap<String, Object>();
|
||||
commentMap.put("user", comment.getUser().getName());
|
||||
commentMap.put("date", comment.getDate());
|
||||
commentMap.put("content", comment.getContent());
|
||||
comments.add(commentMap);
|
||||
}
|
||||
return comments;
|
||||
return PullRequestHelper.getComments(pullRequest);
|
||||
}
|
||||
|
||||
@Path("/get-pull-request-code-comments")
|
||||
|
||||
@ -0,0 +1,102 @@
|
||||
package io.onedev.server.ai;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.PullRequest;
|
||||
import io.onedev.server.model.PullRequestComment;
|
||||
import io.onedev.server.model.PullRequestReview;
|
||||
import io.onedev.server.web.UrlService;
|
||||
|
||||
public class PullRequestHelper {
|
||||
|
||||
private static ObjectMapper getObjectMapper() {
|
||||
return OneDev.getInstance(ObjectMapper.class);
|
||||
}
|
||||
|
||||
private static UrlService getUrlService() {
|
||||
return OneDev.getInstance(UrlService.class);
|
||||
}
|
||||
|
||||
public static Map<String, Object> getSummary(Project currentProject,
|
||||
PullRequest pullRequest, boolean checkMergeConditionIfOpen) {
|
||||
var typeReference = new TypeReference<LinkedHashMap<String, Object>>() {};
|
||||
var summary = getObjectMapper().convertValue(pullRequest, typeReference);
|
||||
summary.remove("id");
|
||||
if (pullRequest.isOpen() && checkMergeConditionIfOpen) {
|
||||
var errorMessage = pullRequest.checkMergeCondition();
|
||||
if (errorMessage != null)
|
||||
summary.put("status", PullRequest.Status.OPEN.name() + " (" + errorMessage + ")");
|
||||
else
|
||||
summary.put("status", PullRequest.Status.OPEN.name() + " (ready to merge)");
|
||||
}
|
||||
summary.remove("uuid");
|
||||
summary.remove("buildCommitHash");
|
||||
summary.remove("submitTimeGroups");
|
||||
summary.remove("closeTimeGroups");
|
||||
summary.remove("checkError");
|
||||
summary.remove("numberScopeId");
|
||||
summary.put("reference", pullRequest.getReference().toString(currentProject));
|
||||
summary.remove("submitterId");
|
||||
summary.put("submitter", pullRequest.getSubmitter().getName());
|
||||
summary.put("targetProject", pullRequest.getTarget().getProject().getPath());
|
||||
if (pullRequest.getSourceProject() != null)
|
||||
summary.put("sourceProject", pullRequest.getSourceProject().getPath());
|
||||
summary.remove("codeCommentsUpdateDate");
|
||||
summary.remove("lastActivity");
|
||||
for (var it = summary.entrySet().iterator(); it.hasNext();) {
|
||||
var entry = it.next();
|
||||
if (entry.getKey().endsWith("Count"))
|
||||
it.remove();
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
public static Map<String, Object> getDetail(Project currentProject, PullRequest pullRequest) {
|
||||
var detail = getSummary(currentProject, pullRequest, true);
|
||||
detail.put("headCommitHash", pullRequest.getLatestUpdate().getHeadCommitHash());
|
||||
detail.put("assignees", pullRequest.getAssignees().stream().map(it->it.getName()).collect(Collectors.toList()));
|
||||
var reviews = new ArrayList<Map<String, Object>>();
|
||||
for (var review : pullRequest.getReviews()) {
|
||||
if (review.getStatus() == PullRequestReview.Status.EXCLUDED)
|
||||
continue;
|
||||
var reviewMap = new HashMap<String, Object>();
|
||||
reviewMap.put("reviewer", review.getUser().getName());
|
||||
reviewMap.put("status", review.getStatus());
|
||||
reviews.add(reviewMap);
|
||||
}
|
||||
detail.put("reviews", reviews);
|
||||
var builds = new ArrayList<String>();
|
||||
for (var build : pullRequest.getBuilds()) {
|
||||
builds.add(build.getReference().toString(currentProject) + " (job: " + build.getJobName() + ", status: " + build.getStatus() + ")");
|
||||
}
|
||||
detail.put("builds", builds);
|
||||
detail.put("labels", pullRequest.getLabels().stream().map(it->it.getSpec().getName()).collect(Collectors.toList()));
|
||||
detail.put("link", getUrlService().urlFor(pullRequest, true));
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
public static List<Map<String, Object>> getComments(PullRequest pullRequest) {
|
||||
var comments = new ArrayList<Map<String, Object>>();
|
||||
pullRequest.getComments().stream().sorted(Comparator.comparing(PullRequestComment::getId)).forEach(comment -> {
|
||||
var commentMap = new HashMap<String, Object>();
|
||||
commentMap.put("user", comment.getUser().getName());
|
||||
commentMap.put("date", comment.getDate());
|
||||
commentMap.put("content", comment.getContent());
|
||||
comments.add(commentMap);
|
||||
});
|
||||
return comments;
|
||||
}
|
||||
|
||||
}
|
||||
@ -102,15 +102,15 @@ public class Chat extends AbstractEntity {
|
||||
}
|
||||
|
||||
public static String getChangeObservable(Long chatId) {
|
||||
return "chat:" + chatId;
|
||||
return Chat.class.getName() + ":" + chatId;
|
||||
}
|
||||
|
||||
public static String getPartialResponseObservable(Long chatId) {
|
||||
return "chat:" + chatId + ":partialResponse";
|
||||
return Chat.class.getName() + ":partialResponse:" + chatId;
|
||||
}
|
||||
|
||||
public static String getNewMessagesObservable(Long chatId) {
|
||||
return "chat:" + chatId + ":newMessages";
|
||||
return Chat.class.getName() + ":newMessages:" + chatId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@ import io.onedev.server.model.Chat;
|
||||
import io.onedev.server.model.ChatMessage;
|
||||
import io.onedev.server.model.User;
|
||||
import io.onedev.server.service.support.ChatResponding;
|
||||
import io.onedev.server.service.support.ChatTool;
|
||||
import io.onedev.server.web.WebSession;
|
||||
|
||||
public interface ChatService extends EntityService<Chat> {
|
||||
|
||||
@ -15,9 +17,13 @@ public interface ChatService extends EntityService<Chat> {
|
||||
|
||||
void createOrUpdate(Chat chat);
|
||||
|
||||
void sendRequest(String sessionId, ChatMessage request);
|
||||
void sendRequest(WebSession session, ChatMessage request, List<ChatTool> tools, int timeoutSeconds);
|
||||
|
||||
@Nullable
|
||||
ChatResponding getResponding(String sessionId, Chat chat);
|
||||
ChatResponding getResponding(WebSession session, Chat chat);
|
||||
|
||||
long nextAnonymousChatId();
|
||||
|
||||
long nextAnonymousChatMessageId();
|
||||
|
||||
}
|
||||
|
||||
@ -1,25 +1,38 @@
|
||||
package io.onedev.server.service.impl;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.apache.commons.lang3.SerializationUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.hibernate.criterion.Order;
|
||||
import org.hibernate.criterion.Restrictions;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.hazelcast.cp.IAtomicLong;
|
||||
|
||||
import dev.langchain4j.data.message.AiMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.ToolExecutionResultMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.chat.request.ChatRequest;
|
||||
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||
import dev.langchain4j.model.chat.response.PartialResponse;
|
||||
import dev.langchain4j.model.chat.response.PartialResponseContext;
|
||||
@ -28,18 +41,26 @@ import dev.langchain4j.model.chat.response.PartialThinkingContext;
|
||||
import dev.langchain4j.model.chat.response.PartialToolCall;
|
||||
import dev.langchain4j.model.chat.response.PartialToolCallContext;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
import io.onedev.commons.utils.StringUtils;
|
||||
import io.onedev.server.cluster.ClusterService;
|
||||
import io.onedev.server.event.Listen;
|
||||
import io.onedev.server.event.system.SystemStarting;
|
||||
import io.onedev.server.exception.ExceptionUtils;
|
||||
import io.onedev.server.model.Chat;
|
||||
import io.onedev.server.model.ChatMessage;
|
||||
import io.onedev.server.model.User;
|
||||
import io.onedev.server.persistence.SessionService;
|
||||
import io.onedev.server.persistence.TransactionService;
|
||||
import io.onedev.server.persistence.annotation.Sessional;
|
||||
import io.onedev.server.persistence.annotation.Transactional;
|
||||
import io.onedev.server.persistence.dao.EntityCriteria;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.service.ChatService;
|
||||
import io.onedev.server.service.support.ChatResponding;
|
||||
import io.onedev.server.service.support.ChatTool;
|
||||
import io.onedev.server.web.SessionListener;
|
||||
import io.onedev.server.web.WebSession;
|
||||
import io.onedev.server.web.websocket.WebSocketService;
|
||||
|
||||
@Singleton
|
||||
@ -62,6 +83,23 @@ public class DefaultChatService extends BaseEntityService<Chat> implements ChatS
|
||||
@Inject
|
||||
private WebSocketService webSocketService;
|
||||
|
||||
@Inject
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Inject
|
||||
private SessionService sessionService;
|
||||
|
||||
@Inject
|
||||
private ClusterService clusterService;
|
||||
|
||||
/*
|
||||
* Use cluster wide id for anonymous chats and messages as we use id to route
|
||||
* websocket observable changes to correct connections
|
||||
*/
|
||||
private volatile IAtomicLong nextAnonymousChatId;
|
||||
|
||||
private volatile IAtomicLong nextAnonymousChatMessageId;
|
||||
|
||||
private final Map<String, Map<Long, ChatRespondingImpl>> respondings = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
@ -70,7 +108,7 @@ public class DefaultChatService extends BaseEntityService<Chat> implements ChatS
|
||||
criteria.add(Restrictions.eq(Chat.PROP_USER, user));
|
||||
criteria.add(Restrictions.eq(Chat.PROP_AI, ai));
|
||||
criteria.add(Restrictions.ilike(Chat.PROP_TITLE, "%" + term + "%"));
|
||||
criteria.addOrder(Order.desc(Chat.PROP_DATE));
|
||||
criteria.addOrder(Order.desc(Chat.PROP_ID));
|
||||
return query(criteria);
|
||||
}
|
||||
|
||||
@ -81,16 +119,22 @@ public class DefaultChatService extends BaseEntityService<Chat> implements ChatS
|
||||
|
||||
@Sessional
|
||||
@Override
|
||||
public ChatResponding getResponding(String sessionId, Chat chat) {
|
||||
return getResponding(sessionId, chat.getId());
|
||||
public ChatResponding getResponding(WebSession session, Chat chat) {
|
||||
return getResponding(session.getId(), chat.getId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void sendRequest(String sessionId, ChatMessage request) {
|
||||
public void sendRequest(WebSession session, ChatMessage request, List<ChatTool> tools, int timeoutSeconds) {
|
||||
var sessionId = session.getId();
|
||||
var requestId = request.getId();
|
||||
var chatId = request.getChat().getId();
|
||||
var anonymous = request.getChat().getUser() == null;
|
||||
|
||||
var modelSetting = request.getChat().getAi().getAiSetting().getModelSetting();
|
||||
var streamingChatModel = modelSetting.getStreamingChatModel();
|
||||
var chatModel = modelSetting.getChatModel();
|
||||
|
||||
var messages = request.getChat().getSortedMessages()
|
||||
.stream()
|
||||
.filter(it->!it.isError())
|
||||
@ -99,123 +143,221 @@ public class DefaultChatService extends BaseEntityService<Chat> implements ChatS
|
||||
if (messages.size() > MAX_HISTORY_MESSAGES)
|
||||
messages = messages.subList(messages.size()-MAX_HISTORY_MESSAGES, messages.size());
|
||||
|
||||
var langchain4jMessages = messages.stream()
|
||||
var langchain4jMessages = new ArrayList<dev.langchain4j.data.message.ChatMessage>();
|
||||
langchain4jMessages.addAll(messages.stream()
|
||||
.map(it -> {
|
||||
var content = it.getContent();
|
||||
if (!it.equals(request))
|
||||
content = StringUtils.abbreviate(content, MAX_HISTORY_MESSAGE_LEN);
|
||||
if (it.isRequest())
|
||||
return new UserMessage(content);
|
||||
return (dev.langchain4j.data.message.ChatMessage) new UserMessage(content);
|
||||
else
|
||||
return new AiMessage(content);
|
||||
return (dev.langchain4j.data.message.ChatMessage) new AiMessage(content);
|
||||
})
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
var toolSpecifications = tools.stream()
|
||||
.map(ChatTool::getSpecification)
|
||||
.collect(Collectors.toList());
|
||||
var completableFuture = new CompletableFuture<String>();
|
||||
var executableFuture = executorService.submit(new Runnable() {
|
||||
|
||||
var future = executorService.submit(() -> {
|
||||
var latch = new CountDownLatch(1);
|
||||
var handler = new StreamingChatResponseHandler() {
|
||||
private StreamingChatResponseHandler newResponseHandler(Subject subject) {
|
||||
return new StreamingChatResponseHandler() {
|
||||
|
||||
private long lastPartialResponseNotificationTime = System.currentTimeMillis();
|
||||
private long lastPartialResponseNotificationTime = System.currentTimeMillis();
|
||||
|
||||
@Override
|
||||
public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) {
|
||||
if (latch.getCount() == 0) {
|
||||
context.streamingHandle().cancel();
|
||||
} else {
|
||||
var responding = getResponding(sessionId, chatId, requestId);
|
||||
if (responding != null) {
|
||||
var content = responding.getContent();
|
||||
if (content == null)
|
||||
content = "";
|
||||
content += partialResponse.text();
|
||||
responding.content = content;
|
||||
if (System.currentTimeMillis() - lastPartialResponseNotificationTime > PARTIAL_RESPONSE_NOTIFICATION_INTERVAL) {
|
||||
lastPartialResponseNotificationTime = System.currentTimeMillis();
|
||||
webSocketService.notifyObservableChange(Chat.getPartialResponseObservable(chatId), null);
|
||||
@Override
|
||||
public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) {
|
||||
ThreadContext.bind(subject);
|
||||
if (completableFuture.isDone()) {
|
||||
context.streamingHandle().cancel();
|
||||
} else {
|
||||
var responding = getResponding(sessionId, chatId, requestId);
|
||||
if (responding != null) {
|
||||
var content = responding.getContent();
|
||||
if (content == null)
|
||||
content = "";
|
||||
content += partialResponse.text();
|
||||
responding.content = content;
|
||||
if (System.currentTimeMillis() - lastPartialResponseNotificationTime > PARTIAL_RESPONSE_NOTIFICATION_INTERVAL) {
|
||||
lastPartialResponseNotificationTime = System.currentTimeMillis();
|
||||
webSocketService.notifyObservableChange(Chat.getPartialResponseObservable(chatId), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialThinking(PartialThinking partialThinking, PartialThinkingContext context) {
|
||||
if (latch.getCount() == 0)
|
||||
context.streamingHandle().cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialToolCall(PartialToolCall partialToolCall, PartialToolCallContext context) {
|
||||
if (latch.getCount() == 0)
|
||||
context.streamingHandle().cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse completeResponse) {
|
||||
try {
|
||||
createResponseIfNecessary(sessionId, chatId, requestId, completeResponse.aiMessage().text(), null);
|
||||
} finally {
|
||||
latch.countDown();
|
||||
@Override
|
||||
public void onPartialThinking(PartialThinking partialThinking, PartialThinkingContext context) {
|
||||
ThreadContext.bind(subject);
|
||||
if (completableFuture.isDone())
|
||||
context.streamingHandle().cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
try {
|
||||
createResponseIfNecessary(sessionId, chatId, requestId, "Error getting chat response, check server log for details", error);
|
||||
} finally {
|
||||
latch.countDown();
|
||||
@Override
|
||||
public void onPartialToolCall(PartialToolCall partialToolCall, PartialToolCallContext context) {
|
||||
ThreadContext.bind(subject);
|
||||
if (completableFuture.isDone()) {
|
||||
context.streamingHandle().cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
try {
|
||||
modelSetting.getStreamingChatModel().chat(langchain4jMessages, handler);
|
||||
transactionService.run(() -> {
|
||||
var chat = load(chatId);
|
||||
var requests = chat.getMessages().stream().filter(it->it.isRequest()).collect(Collectors.toList());
|
||||
if (requests.size() == 1) {
|
||||
var systemPrompt = String.format("""
|
||||
Summarize provided message to get a compact title with below requirements:
|
||||
1. It should be within %d characters
|
||||
2. Only title is returned, no other text or comments
|
||||
""", Chat.MAX_TITLE_LEN);
|
||||
var title = modelSetting.getChatModel().chat(new SystemMessage(systemPrompt), new UserMessage(requests.get(0).getContent())).aiMessage().text();
|
||||
chat.setTitle(title);
|
||||
webSocketService.notifyObservableChange(Chat.getChangeObservable(chatId), null);
|
||||
}
|
||||
});
|
||||
latch.await();
|
||||
} catch (Exception e) {
|
||||
if (ExceptionUtils.find(e, InterruptedException.class) == null) {
|
||||
logger.error("Error getting chat response", e);
|
||||
String errorMessage = e.getMessage();
|
||||
if (errorMessage == null)
|
||||
errorMessage = "Error getting chat response, check server log for details";
|
||||
createResponseIfNecessary(sessionId, chatId, requestId, errorMessage, e);
|
||||
} else {
|
||||
var responding = getResponding(sessionId, chatId, requestId);
|
||||
if (responding != null && responding.getContent() != null)
|
||||
createResponseIfNecessary(sessionId, chatId, requestId, responding.getContent(), null);
|
||||
}
|
||||
} finally {
|
||||
latch.countDown();
|
||||
var respondingsOfSession = respondings.get(sessionId);
|
||||
if (respondingsOfSession != null) {
|
||||
var responding = respondingsOfSession.get(chatId);
|
||||
if (responding != null) {
|
||||
respondingsOfSession.computeIfPresent(chatId, (k, v)-> {
|
||||
if (v.requestId.equals(requestId))
|
||||
return null;
|
||||
else
|
||||
return v;
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse completeResponse) {
|
||||
ThreadContext.bind(subject);
|
||||
sessionService.runAsync(() -> {
|
||||
try {
|
||||
var aiMessage = completeResponse.aiMessage();
|
||||
if (aiMessage.hasToolExecutionRequests()) {
|
||||
langchain4jMessages.add(aiMessage);
|
||||
var toolRequests = aiMessage.toolExecutionRequests();
|
||||
for (int i = 0; i < toolRequests.size(); i++) {
|
||||
var toolRequest = toolRequests.get(i);
|
||||
String toolName = toolRequest.name();
|
||||
String toolArgs = toolRequest.arguments();
|
||||
var toolResult = tools.stream()
|
||||
.filter(it->it.getSpecification().name().equals(toolName))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ExplicitException("Unknown tool: " + toolName))
|
||||
.execute(objectMapper.readTree(toolArgs));
|
||||
ToolExecutionResultMessage toolResultMessage = ToolExecutionResultMessage.from(
|
||||
toolRequest.id(), toolName, toolResult);
|
||||
langchain4jMessages.add(toolResultMessage);
|
||||
}
|
||||
|
||||
var handler = newResponseHandler(SecurityUtils.getSubject());
|
||||
var langchain4jChatRequest = ChatRequest.builder()
|
||||
.messages(new ArrayList<>(langchain4jMessages))
|
||||
.toolSpecifications(toolSpecifications)
|
||||
.build();
|
||||
streamingChatModel.chat(langchain4jChatRequest, handler);
|
||||
} else {
|
||||
completableFuture.complete(aiMessage.text());
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
completableFuture.completeExceptionally(t);
|
||||
}
|
||||
});
|
||||
webSocketService.notifyObservableChange(Chat.getPartialResponseObservable(chatId), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
ThreadContext.bind(subject);
|
||||
completableFuture.completeExceptionally(error);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
private void createResponse(String content, @Nullable Throwable throwable) {
|
||||
var responding = getResponding(session.getId(), chatId, requestId);
|
||||
if (responding != null) {
|
||||
transactionService.run(() -> {
|
||||
if (throwable != null)
|
||||
logger.error("Error getting chat response", throwable);
|
||||
var response = new ChatMessage();
|
||||
response.setError(throwable != null);
|
||||
response.setContent(content);
|
||||
if (anonymous) {
|
||||
var chat = SerializationUtils.clone(checkNotNull(session.getAnonymousChats().get(chatId)));
|
||||
response.setChat(chat);
|
||||
response.setId(nextAnonymousChatMessageId());
|
||||
chat.getMessages().add(response);
|
||||
session.getAnonymousChats().put(chatId, chat);
|
||||
} else {
|
||||
response.setChat(load(chatId));
|
||||
dao.persist(response);
|
||||
}
|
||||
webSocketService.notifyObservableChange(Chat.getNewMessagesObservable(chatId), null);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private void createIncompleteResponse(String reason) {
|
||||
var responding = getResponding(sessionId, chatId, requestId);
|
||||
if (responding != null) {
|
||||
var content = responding.getContent();
|
||||
if (content == null)
|
||||
content = "";
|
||||
else
|
||||
content += "\n\n";
|
||||
content += "<b class='text text-danger'>" + reason + "</b>";
|
||||
createResponse(content, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
var handler = newResponseHandler(SecurityUtils.getSubject());
|
||||
try {
|
||||
var langchain4jChatRequest = ChatRequest.builder()
|
||||
.messages(langchain4jMessages.stream()
|
||||
.map(msg -> (dev.langchain4j.data.message.ChatMessage) msg)
|
||||
.collect(Collectors.toList()))
|
||||
.toolSpecifications(toolSpecifications)
|
||||
.build();
|
||||
|
||||
streamingChatModel.chat(langchain4jChatRequest, handler);
|
||||
transactionService.run(() -> {
|
||||
var chat = anonymous ? checkNotNull(session.getAnonymousChats().get(chatId)) : load(chatId);
|
||||
var requests = chat.getMessages().stream().filter(it->it.isRequest()).collect(Collectors.toList());
|
||||
if (requests.size() == 1) {
|
||||
var systemPrompt = String.format("""
|
||||
Summarize provided message to get a compact title with below requirements:
|
||||
1. Just summarize the message itself, no need to ask user for clarification or confirmation
|
||||
2. It should be within %d characters
|
||||
3. Only title is returned, no other text or comments
|
||||
4. Display in %s
|
||||
""", Chat.MAX_TITLE_LEN, session.getLocale().getDisplayLanguage());
|
||||
var title = chatModel.chat(new SystemMessage(systemPrompt), new UserMessage(requests.get(0).getContent())).aiMessage().text();
|
||||
if (anonymous) {
|
||||
chat = SerializationUtils.clone(chat);
|
||||
chat.setTitle(title);
|
||||
session.getAnonymousChats().put(chatId, chat);
|
||||
} else {
|
||||
chat.setTitle(title);
|
||||
dao.persist(chat);
|
||||
}
|
||||
webSocketService.notifyObservableChange(Chat.getChangeObservable(chatId), null);
|
||||
}
|
||||
});
|
||||
var response = completableFuture.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||
if (StringUtils.isBlank(response))
|
||||
throw new ExplicitException("Received empty response");
|
||||
createResponse(response, null);
|
||||
} catch (Throwable e) {
|
||||
if (ExceptionUtils.find(e, InterruptedException.class) != null) {
|
||||
createIncompleteResponse("Conversation cancelled");
|
||||
} else if (ExceptionUtils.find(e, TimeoutException.class) != null) {
|
||||
createIncompleteResponse("Conversation timed out");
|
||||
} else {
|
||||
logger.error("Error getting chat response", e);
|
||||
String errorMessage = e.getMessage();
|
||||
if (errorMessage == null)
|
||||
errorMessage = "Error getting chat response, check server log for details";
|
||||
createResponse(errorMessage, e);
|
||||
}
|
||||
} finally {
|
||||
completableFuture.cancel(false);
|
||||
var respondingsOfSession = respondings.get(sessionId);
|
||||
if (respondingsOfSession != null) {
|
||||
var responding = respondingsOfSession.get(chatId);
|
||||
if (responding != null) {
|
||||
respondingsOfSession.computeIfPresent(chatId, (k, v)-> {
|
||||
if (v.requestId.equals(requestId))
|
||||
return null;
|
||||
else
|
||||
return v;
|
||||
});
|
||||
webSocketService.notifyObservableChange(Chat.getPartialResponseObservable(chatId), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var previousResponding = respondings.computeIfAbsent(sessionId, it->new ConcurrentHashMap<>()).put(chatId, new ChatRespondingImpl(requestId, future));
|
||||
var previousResponding = respondings.computeIfAbsent(sessionId, it->new ConcurrentHashMap<>()).put(chatId, new ChatRespondingImpl(requestId, executableFuture));
|
||||
if (previousResponding != null)
|
||||
previousResponding.cancel();
|
||||
}
|
||||
@ -236,21 +378,24 @@ public class DefaultChatService extends BaseEntityService<Chat> implements ChatS
|
||||
return null;
|
||||
}
|
||||
|
||||
private void createResponseIfNecessary(String sessionId, Long chatId, Long requestId, String content, @Nullable Throwable throwable) {
|
||||
var responding = getResponding(sessionId, chatId, requestId);
|
||||
if (responding != null) {
|
||||
transactionService.run(() -> {
|
||||
var chat = load(chatId);
|
||||
if (throwable != null)
|
||||
logger.error("Error getting chat response", throwable);
|
||||
var response = new ChatMessage();
|
||||
response.setChat(chat);
|
||||
response.setError(throwable != null);
|
||||
response.setContent(content);
|
||||
dao.persist(response);
|
||||
webSocketService.notifyObservableChange(Chat.getNewMessagesObservable(chatId), null);
|
||||
});
|
||||
};
|
||||
@Listen
|
||||
public void on(SystemStarting event) {
|
||||
nextAnonymousChatId = clusterService.getHazelcastInstance().getCPSubsystem().getAtomicLong("nextAnonymousChatId");
|
||||
nextAnonymousChatMessageId = clusterService.getHazelcastInstance().getCPSubsystem().getAtomicLong("nextAnonymousChatMessageId");
|
||||
if (clusterService.isLeaderServer()) {
|
||||
nextAnonymousChatId.set(1L);
|
||||
nextAnonymousChatMessageId.set(1L);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nextAnonymousChatId() {
|
||||
return nextAnonymousChatId.getAndIncrement();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nextAnonymousChatMessageId() {
|
||||
return nextAnonymousChatMessageId.getAndIncrement();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package io.onedev.server.service.support;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
|
||||
public interface ChatTool {
|
||||
|
||||
ToolSpecification getSpecification();
|
||||
|
||||
/**
|
||||
* This method should not rely on any Wicket facilities, such as page, component, model etc.
|
||||
* If you need to access some data from these facilities, make sure to prepare them while
|
||||
* creating the tool
|
||||
*
|
||||
* @param arguments
|
||||
* @return
|
||||
*/
|
||||
String execute(JsonNode arguments);
|
||||
|
||||
}
|
||||
@ -5,7 +5,6 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
@ -14,8 +13,10 @@ import org.apache.shiro.subject.Subject;
|
||||
import org.apache.wicket.protocol.http.WicketServlet;
|
||||
import org.apache.wicket.request.Request;
|
||||
import org.apache.wicket.util.collections.ConcurrentHashSet;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.model.Chat;
|
||||
import io.onedev.server.web.util.Cursor;
|
||||
|
||||
public class WebSession extends org.apache.wicket.protocol.http.WebSession {
|
||||
@ -36,11 +37,13 @@ public class WebSession extends org.apache.wicket.protocol.http.WebSession {
|
||||
|
||||
private Set<Long> expandedProjectIds = new ConcurrentHashSet<>();
|
||||
|
||||
private boolean chatVisible;
|
||||
private volatile boolean chatVisible;
|
||||
|
||||
private Long activeChatId;
|
||||
private volatile Long activeChatId;
|
||||
|
||||
private String chatInput;
|
||||
private volatile Map<Long, Chat> anonymousChats = new ConcurrentHashMap<>();
|
||||
|
||||
private volatile String chatInput;
|
||||
|
||||
public WebSession(Request request) {
|
||||
super(request);
|
||||
@ -141,6 +144,10 @@ public class WebSession extends org.apache.wicket.protocol.http.WebSession {
|
||||
this.activeChatId = activeChatId;
|
||||
}
|
||||
|
||||
public Map<Long, Chat> getAnonymousChats() {
|
||||
return anonymousChats;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getChatInput() {
|
||||
return chatInput;
|
||||
|
||||
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764891371306" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9180" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M789.333333 512V234.666667q0-42.666667-42.666666-42.666667H234.666667q-42.666667 0-42.666667 42.666667v554.666666q0 42.666667 42.666667 42.666667h298.666666a42.666667 42.666667 0 1 1 0 85.333333H234.666667q-53.013333 0-90.517334-37.482666Q106.666667 842.346667 106.666667 789.333333V234.666667q0-53.013333 37.482666-90.517334Q181.653333 106.666667 234.666667 106.666667h512q53.013333 0 90.517333 37.482666Q874.666667 181.653333 874.666667 234.666667v277.333333a42.666667 42.666667 0 1 1-85.333334 0zM490.666667 277.333333h-170.666667a42.666667 42.666667 0 1 0 0 85.333334h170.666667a42.666667 42.666667 0 1 0 0-85.333334z m213.333333 213.333334a42.666667 42.666667 0 0 0-42.666667-42.666667H320a42.666667 42.666667 0 1 0 0 85.333333h341.333333a42.666667 42.666667 0 0 0 42.666667-42.666666zM320 704h85.333333a42.666667 42.666667 0 1 0 0-85.333333h-85.333333a42.666667 42.666667 0 1 0 0 85.333333z m310.442667 79.36c-15.701333-4.202667-15.701333-26.517333 0-30.72l7.722666-2.090667a159.274667 159.274667 0 0 0 112.384-112.384l2.069334-7.722666c4.224-15.701333 26.538667-15.701333 30.762666 0l2.069334 7.722666a159.274667 159.274667 0 0 0 112.384 112.384l7.701333 2.069334c15.722667 4.224 15.722667 26.538667 0 30.762666l-7.68 2.069334a159.274667 159.274667 0 0 0-112.426667 112.384l-2.048 7.701333c-4.224 15.722667-26.538667 15.722667-30.762666 0l-2.069334-7.68a159.274667 159.274667 0 0 0-112.384-112.405333l-7.722666-2.069334z" p-id="9181"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -1,8 +1,8 @@
|
||||
<wicket:panel>
|
||||
<div class="ui-resizable-handle ui-resizable-w"></div>
|
||||
<div class="head d-flex align-items-center justify-content-between px-4 py-3 border-bottom">
|
||||
<h5 class="card-title mb-0"><wicket:t>Chat with</wicket:t> <a wicket:id="aiSelector" class="ml-2"><img wicket:id="avatar"></img> <span wicket:id="name"></span> <wicket:svg href="arrow" class="icon rotate-90"></wicket:svg></a></h5>
|
||||
<a wicket:id="close" t:data-tippy-content="Close" class="btn btn-xs btn-icon btn-light btn-hover-primary"><wicket:svg href="times" class="icon"/></a>
|
||||
<div class="head d-flex align-items-center px-4 py-3 border-bottom">
|
||||
<h5 class="card-title mb-0 mr-3"><wicket:t>Chat with</wicket:t> <a wicket:id="aiSelector" class="ml-2"><img wicket:id="avatar"></img> <span wicket:id="name"></span> <wicket:svg href="arrow" class="icon rotate-90"></wicket:svg></a></h5>
|
||||
<a wicket:id="close" t:data-tippy-content="Close" class="ml-auto btn btn-xs btn-icon btn-light btn-hover-primary"><wicket:svg href="times" class="icon"/></a>
|
||||
</div>
|
||||
<div class="body d-flex flex-column flex-grow-1 p-4 position-relative" style="overflow: hidden;">
|
||||
<div wicket:id="chatSelectorContainer" class="chat-selector d-flex flex-shrink-0 mb-3">
|
||||
|
||||
@ -6,6 +6,7 @@ import static io.onedev.server.web.translation.Translation._T;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
@ -13,6 +14,7 @@ import java.util.stream.Collectors;
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.Cookie;
|
||||
|
||||
import org.apache.commons.lang3.SerializationUtils;
|
||||
import org.apache.wicket.Component;
|
||||
import org.apache.wicket.ajax.AjaxRequestTarget;
|
||||
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
|
||||
@ -31,10 +33,13 @@ import org.apache.wicket.markup.html.panel.Panel;
|
||||
import org.apache.wicket.markup.repeater.RepeatingView;
|
||||
import org.apache.wicket.model.AbstractReadOnlyModel;
|
||||
import org.apache.wicket.model.IModel;
|
||||
import org.apache.wicket.model.LoadableDetachableModel;
|
||||
import org.apache.wicket.model.Model;
|
||||
import org.apache.wicket.request.cycle.RequestCycle;
|
||||
import org.apache.wicket.request.http.WebRequest;
|
||||
import org.apache.wicket.request.http.WebResponse;
|
||||
import org.apache.wicket.util.visit.IVisit;
|
||||
import org.apache.wicket.util.visit.IVisitor;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONWriter;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@ -46,6 +51,8 @@ import io.onedev.server.persistence.dao.Dao;
|
||||
import io.onedev.server.service.ChatService;
|
||||
import io.onedev.server.service.UserService;
|
||||
import io.onedev.server.service.support.ChatResponding;
|
||||
import io.onedev.server.service.support.ChatTool;
|
||||
import io.onedev.server.util.facade.UserFacade;
|
||||
import io.onedev.server.web.WebConstants;
|
||||
import io.onedev.server.web.WebSession;
|
||||
import io.onedev.server.web.behavior.ChangeObserver;
|
||||
@ -59,11 +66,14 @@ import io.onedev.server.web.component.select2.ChoiceProvider;
|
||||
import io.onedev.server.web.component.select2.ResponseFiller;
|
||||
import io.onedev.server.web.component.select2.Select2Choice;
|
||||
import io.onedev.server.web.component.user.UserAvatar;
|
||||
import io.onedev.server.web.util.ChatToolAware;
|
||||
|
||||
public class ChatPanel extends Panel {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final int TIMEOUT_SECONDS = 120;
|
||||
|
||||
private static final String COOKIE_ACTIVE_AI = "active-ai";
|
||||
|
||||
@Inject
|
||||
@ -81,6 +91,23 @@ public class ChatPanel extends Panel {
|
||||
|
||||
private WebMarkupContainer respondingContainer;
|
||||
|
||||
private final IModel<List<User>> entitledAisModel = new LoadableDetachableModel<List<User>>() {
|
||||
|
||||
@Override
|
||||
protected List<User> load() {
|
||||
if (getUser() != null) {
|
||||
return getUser().getEntitledAis();
|
||||
} else {
|
||||
return userService.cloneCache().values().stream()
|
||||
.filter(it -> it.getType() == User.Type.AI && !it.isDisabled() && it.isEntitleToAll())
|
||||
.sorted(Comparator.comparing(UserFacade::getDisplayName))
|
||||
.map(it->userService.load(it.getId()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public ChatPanel(String componentId) {
|
||||
super(componentId);
|
||||
|
||||
@ -100,7 +127,7 @@ public class ChatPanel extends Panel {
|
||||
protected List<MenuItem> getMenuItems(FloatingPanel dropdown) {
|
||||
var activeAI = getActiveAI();
|
||||
var menuItems = new ArrayList<MenuItem>();
|
||||
for (var ai : getUser().getEntitledAis()) {
|
||||
for (var ai : getEntitledAis()) {
|
||||
menuItems.add(new MenuItem() {
|
||||
|
||||
@Override
|
||||
@ -172,7 +199,16 @@ public class ChatPanel extends Panel {
|
||||
@Override
|
||||
public void query(String term, int page, io.onedev.server.web.component.select2.Response<Chat> response) {
|
||||
var count = (page+1) * WebConstants.PAGE_SIZE + 1;
|
||||
var chats = chatService.query(getUser(), getActiveAI(), term, count);
|
||||
List<Chat> chats;
|
||||
if (getUser() != null) {
|
||||
chats = chatService.query(getUser(), getActiveAI(), term, count);
|
||||
} else {
|
||||
chats = WebSession.get().getAnonymousChats().values().stream()
|
||||
.filter(it -> it.getAi().equals(getActiveAI()) && it.getTitle().toLowerCase().contains(term.toLowerCase()))
|
||||
.sorted(Comparator.comparing(Chat::getId).reversed())
|
||||
.limit(count)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
new ResponseFiller<>(response).fill(chats, page, WebConstants.PAGE_SIZE);
|
||||
}
|
||||
|
||||
@ -205,10 +241,6 @@ public class ChatPanel extends Panel {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
public void onObservableChanged(IPartialPageRequestHandler handler, Collection<String> changedObservables) {
|
||||
handler.add(chatSelectorContainer);
|
||||
}
|
||||
|
||||
});
|
||||
chatSelectorContainer.add(new ChangeObserver() {
|
||||
|
||||
@ -223,10 +255,11 @@ public class ChatPanel extends Panel {
|
||||
|
||||
@Override
|
||||
public void onObservableChanged(IPartialPageRequestHandler handler, Collection<String> changedObservables) {
|
||||
showNewMessages(handler);
|
||||
if (isVisible())
|
||||
showNewMessages(handler);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
chatSelectorContainer.add(new AjaxLink<Void>("newChat") {
|
||||
|
||||
@ -242,7 +275,10 @@ public class ChatPanel extends Panel {
|
||||
@Override
|
||||
public void onClick(AjaxRequestTarget target) {
|
||||
getSession().success(_T("Chat deleted"));
|
||||
chatService.delete(getActiveChat());
|
||||
if (getUser() != null)
|
||||
chatService.delete(getActiveChat());
|
||||
else
|
||||
WebSession.get().getAnonymousChats().remove(getActiveChat().getId());
|
||||
WebSession.get().setActiveChatId(null);
|
||||
target.add(ChatPanel.this);
|
||||
}
|
||||
@ -337,23 +373,55 @@ public class ChatPanel extends Panel {
|
||||
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
|
||||
var input = WebSession.get().getChatInput().trim();
|
||||
var chat = getActiveChat();
|
||||
ChatMessage request = new ChatMessage();
|
||||
request.setRequest(true);
|
||||
request.setContent(input);
|
||||
if (chat == null) {
|
||||
chat = new Chat();
|
||||
chat.setUser(getUser());
|
||||
chat.setAi(getActiveAI());
|
||||
chat.setTitle(_T("New chat"));
|
||||
chat.setDate(new Date());
|
||||
chatService.createOrUpdate(chat);
|
||||
if (getUser() != null) {
|
||||
chatService.createOrUpdate(chat);
|
||||
request.setChat(chat);
|
||||
chat.getMessages().add(request);
|
||||
dao.persist(request);
|
||||
} else {
|
||||
chat.setId(chatService.nextAnonymousChatId());
|
||||
request.setId(chatService.nextAnonymousChatMessageId());
|
||||
request.setChat(chat);
|
||||
chat.getMessages().add(request);
|
||||
WebSession.get().getAnonymousChats().put(chat.getId(), chat);
|
||||
}
|
||||
WebSession.get().setActiveChatId(chat.getId());
|
||||
target.add(chatSelectorContainer);
|
||||
} else {
|
||||
if (getUser() != null) {
|
||||
request.setChat(chat);
|
||||
chat.getMessages().add(request);
|
||||
dao.persist(request);
|
||||
} else {
|
||||
request.setId(chatService.nextAnonymousChatMessageId());
|
||||
chat = SerializationUtils.clone(chat);
|
||||
request.setChat(chat);
|
||||
chat.getMessages().add(request);
|
||||
WebSession.get().getAnonymousChats().put(chat.getId(), chat);
|
||||
}
|
||||
}
|
||||
var request = new ChatMessage();
|
||||
request.setChat(chat);
|
||||
request.setRequest(true);
|
||||
request.setContent(input);
|
||||
chat.getMessages().add(request);
|
||||
dao.persist(request);
|
||||
chatService.sendRequest(getSession().getId(), request);
|
||||
|
||||
List<ChatTool> chatTools = new ArrayList<>();
|
||||
if (getPage() instanceof ChatToolAware)
|
||||
chatTools.addAll(((ChatToolAware) getPage()).getChatTools());
|
||||
getPage().visitChildren(ChatToolAware.class, new IVisitor<Component, Void>() {
|
||||
|
||||
@Override
|
||||
public void component(Component component, IVisit<Void> visit) {
|
||||
chatTools.addAll(((ChatToolAware) component).getChatTools());
|
||||
}
|
||||
|
||||
});
|
||||
chatService.sendRequest(WebSession.get(), request, chatTools, TIMEOUT_SECONDS);
|
||||
|
||||
showNewMessages(target);
|
||||
target.add(respondingContainer);
|
||||
@ -388,10 +456,10 @@ public class ChatPanel extends Panel {
|
||||
private User getActiveAI() {
|
||||
if (activeAiId != null) {
|
||||
var ai = userService.get(activeAiId);
|
||||
if (ai != null && getUser().getEntitledAis().contains(ai))
|
||||
if (ai != null && getEntitledAis().contains(ai))
|
||||
return ai;
|
||||
}
|
||||
return getUser().getEntitledAis().get(0);
|
||||
return getEntitledAis().get(0);
|
||||
}
|
||||
|
||||
private void setActiveAI(User ai) {
|
||||
@ -408,7 +476,11 @@ public class ChatPanel extends Panel {
|
||||
private Chat getActiveChat() {
|
||||
var activeChatId = WebSession.get().getActiveChatId();
|
||||
if (activeChatId != null) {
|
||||
var chat = chatService.get(activeChatId);
|
||||
Chat chat;
|
||||
if (getUser() != null)
|
||||
chat = chatService.get(activeChatId);
|
||||
else
|
||||
chat = WebSession.get().getAnonymousChats().get(activeChatId);
|
||||
if (chat != null && chat.getAi().equals(getActiveAI()))
|
||||
return chat;
|
||||
}
|
||||
@ -419,7 +491,7 @@ public class ChatPanel extends Panel {
|
||||
private ChatResponding getResponding() {
|
||||
var chat = getActiveChat();
|
||||
if (chat != null)
|
||||
return chatService.getResponding(getSession().getId(), chat);
|
||||
return chatService.getResponding(WebSession.get(), chat);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
@ -434,12 +506,13 @@ public class ChatPanel extends Panel {
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void showNewMessages(IPartialPageRequestHandler handler) {
|
||||
long lastMessageId;
|
||||
long prevLastMessageId;
|
||||
if (messagesView.size() != 0)
|
||||
lastMessageId = (Long) messagesView.get(messagesView.size() - 1).getDefaultModelObject();
|
||||
prevLastMessageId = (Long) messagesView.get(messagesView.size() - 1).getDefaultModelObject();
|
||||
else
|
||||
lastMessageId = 0;
|
||||
getMessages().stream().filter(it -> it.getId() > lastMessageId).forEach(it -> {
|
||||
prevLastMessageId = 0;
|
||||
|
||||
getMessages().stream().filter(it -> it.getId() > prevLastMessageId).forEach(it -> {
|
||||
var messageContainer = newMessageContainer(messagesView.newChildId(), it);
|
||||
messagesView.add(messageContainer);
|
||||
handler.prependJavaScript(String.format("""
|
||||
@ -447,10 +520,13 @@ public class ChatPanel extends Panel {
|
||||
""", respondingContainer.getMarkupId(), messageContainer.getMarkupId()));
|
||||
handler.add(messageContainer);
|
||||
});
|
||||
|
||||
var lastMessage = messagesView.get(messagesView.size() - 1);
|
||||
handler.appendJavaScript(String.format("""
|
||||
$('#%s')[0].scrollIntoView({ block: "end" });
|
||||
""", lastMessage.getMarkupId()));
|
||||
if ((Long) lastMessage.getDefaultModelObject() > prevLastMessageId) {
|
||||
handler.appendJavaScript(String.format("""
|
||||
$('#%s')[0].scrollIntoView({ block: "end" });
|
||||
""", lastMessage.getMarkupId()));
|
||||
}
|
||||
}
|
||||
|
||||
private Component newMessageContainer(String containerId, ChatMessage message) {
|
||||
@ -493,7 +569,7 @@ public class ChatPanel extends Panel {
|
||||
@Override
|
||||
protected void onConfigure() {
|
||||
super.onConfigure();
|
||||
setVisible(WebSession.get().isChatVisible());
|
||||
setVisible(WebSession.get().isChatVisible() && !getEntitledAis().isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -504,16 +580,25 @@ public class ChatPanel extends Panel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVisible() {
|
||||
return WebSession.get().isChatVisible();
|
||||
protected void onDetach() {
|
||||
super.onDetach();
|
||||
entitledAisModel.detach();
|
||||
}
|
||||
|
||||
public void show(AjaxRequestTarget target) {
|
||||
if (!isVisible()) {
|
||||
WebSession.get().setChatVisible(true);
|
||||
WebSession.get().setActiveChatId(null);
|
||||
target.add(this);
|
||||
public List<User> getEntitledAis() {
|
||||
return entitledAisModel.getObject();
|
||||
}
|
||||
|
||||
public void show(AjaxRequestTarget target, @Nullable String prompt) {
|
||||
WebSession.get().setChatVisible(true);
|
||||
WebSession.get().setActiveChatId(null);
|
||||
if (prompt != null) {
|
||||
WebSession.get().setChatInput(prompt);
|
||||
target.appendJavaScript("""
|
||||
$(".chat>.body>.send .submit").click();
|
||||
""");
|
||||
}
|
||||
target.add(this);
|
||||
}
|
||||
|
||||
public void hide(AjaxRequestTarget target) {
|
||||
|
||||
@ -1,25 +1,20 @@
|
||||
.chat {
|
||||
position: fixed;
|
||||
top: calc(var(--topbar-height) + 1rem);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
border-radius: 0.42rem 0 0 0;
|
||||
z-index: 2000;
|
||||
background: white;
|
||||
box-shadow: 0 0 12px rgba(0,0,0,0.2), 0 -2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.chat>.head {
|
||||
border-radius: 0.42rem 0 0 0;
|
||||
box-shadow: 0 0 12px rgba(0,0,0,0.2);
|
||||
}
|
||||
.dark-mode .chat {
|
||||
background-color: var(--dark-mode-dark);
|
||||
box-shadow: 0 0 20px rgb(0 0 0 / 50%), 0 -2px 15px rgb(0 0 0 / 30%);
|
||||
box-shadow: 0 0 20px rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
.chat .ui-resizable-handle {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.42rem;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
cursor: col-resize;
|
||||
|
||||
@ -18,10 +18,11 @@
|
||||
</form>
|
||||
</wicket:fragment>
|
||||
<wicket:fragment wicket:id="optionsFrag">
|
||||
<div class="issue-activity-options">
|
||||
<a wicket:id="showComments" t:data-tippy-content="Toggle comments" class="btn btn-xs text-muted btn-active-secondary btn-icon"><wicket:svg href="comments" class="icon"></wicket:svg></a>
|
||||
<a wicket:id="showChangeHistory" t:data-tippy-content="Toggle change history" class="btn btn-xs text-muted btn-active-secondary btn-icon"><wicket:svg href="history" class="icon"></wicket:svg></a>
|
||||
<a wicket:id="showWorkLog" t:data-tippy-content="Toggle work log" class="btn btn-xs text-muted btn-active-secondary btn-icon"><wicket:svg href="info" class="icon"></wicket:svg></a>
|
||||
<a wicket:id="aiSummarize" t:data-tippy-content="Summarize comments with AI" class="btn btn-xs btn-outline-secondary btn-hover-primary btn-icon"><wicket:svg href="ai-summarize" class="icon"></wicket:svg></a>
|
||||
<div class="btn-group">
|
||||
<a wicket:id="showComments" t:data-tippy-content="Toggle comments" class="btn btn-xs text-muted btn-outline-secondary btn-active-secondary btn-icon"><wicket:svg href="comments" class="icon"></wicket:svg></a>
|
||||
<a wicket:id="showChangeHistory" t:data-tippy-content="Toggle change history" class="btn btn-xs text-muted btn-outline-secondary btn-active-secondary btn-icon"><wicket:svg href="history" class="icon"></wicket:svg></a>
|
||||
<a wicket:id="showWorkLog" t:data-tippy-content="Toggle work log" class="btn btn-xs text-muted btn-outline-secondary btn-active-secondary btn-icon"><wicket:svg href="info" class="icon"></wicket:svg></a>
|
||||
</div>
|
||||
</wicket:fragment>
|
||||
</wicket:panel>
|
||||
@ -39,7 +39,6 @@ import com.google.common.collect.Lists;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.attachment.AttachmentSupport;
|
||||
import io.onedev.server.attachment.ProjectAttachmentSupport;
|
||||
import io.onedev.server.service.IssueCommentService;
|
||||
import io.onedev.server.entityreference.ReferencedFromAware;
|
||||
import io.onedev.server.model.Issue;
|
||||
import io.onedev.server.model.IssueChange;
|
||||
@ -53,6 +52,7 @@ import io.onedev.server.model.support.issue.changedata.IssueOwnSpentTimeChangeDa
|
||||
import io.onedev.server.model.support.issue.changedata.IssueTotalEstimatedTimeChangeData;
|
||||
import io.onedev.server.model.support.issue.changedata.IssueTotalSpentTimeChangeData;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.service.IssueCommentService;
|
||||
import io.onedev.server.web.ajaxlistener.ConfirmLeaveListener;
|
||||
import io.onedev.server.web.behavior.ChangeObserver;
|
||||
import io.onedev.server.web.component.comment.CommentInput;
|
||||
@ -61,6 +61,7 @@ import io.onedev.server.web.component.issue.activities.activity.IssueChangeActiv
|
||||
import io.onedev.server.web.component.issue.activities.activity.IssueCommentActivity;
|
||||
import io.onedev.server.web.component.issue.activities.activity.IssueWorkActivity;
|
||||
import io.onedev.server.web.page.base.BasePage;
|
||||
import io.onedev.server.web.page.layout.LayoutPage;
|
||||
import io.onedev.server.web.page.security.LoginPage;
|
||||
import io.onedev.server.web.util.WicketUtils;
|
||||
|
||||
@ -327,6 +328,23 @@ public abstract class IssueActivitiesPanel extends Panel {
|
||||
|
||||
public Component renderOptions(String componentId) {
|
||||
Fragment fragment = new Fragment(componentId, "optionsFrag", this);
|
||||
|
||||
fragment.add(new AjaxLink<Void>("aiSummarize") {
|
||||
|
||||
@Override
|
||||
public void onClick(AjaxRequestTarget target) {
|
||||
var page = (LayoutPage)getPage();
|
||||
page.getChatter().show(target, "Summarize comments of current issue. Display summary in " + getSession().getLocale().getDisplayLanguage());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigure() {
|
||||
super.onConfigure();
|
||||
var page = (LayoutPage)getPage();
|
||||
setVisible(!page.getChatter().getEntitledAis().isEmpty());
|
||||
}
|
||||
});
|
||||
|
||||
fragment.add(showCommentsLink = new AjaxLink<Void>("showComments") {
|
||||
|
||||
@Override
|
||||
|
||||
@ -176,6 +176,8 @@ public abstract class LayoutPage extends BasePage {
|
||||
|
||||
private AbstractDefaultAjaxBehavior newVersionStatusBehavior;
|
||||
|
||||
private ChatPanel chatter;
|
||||
|
||||
public LayoutPage(PageParameters params) {
|
||||
super(params);
|
||||
}
|
||||
@ -811,20 +813,20 @@ public abstract class LayoutPage extends BasePage {
|
||||
|
||||
});
|
||||
|
||||
var chat = new ChatPanel("chat");
|
||||
add(chat);
|
||||
chatter = new ChatPanel("chat");
|
||||
add(chatter);
|
||||
|
||||
topbar.add(new AjaxLink<Void>("showChat") {
|
||||
|
||||
@Override
|
||||
public void onClick(AjaxRequestTarget target) {
|
||||
chat.show(target);
|
||||
chatter.show(target, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigure() {
|
||||
super.onConfigure();
|
||||
setVisible(getLoginUser() != null && !getLoginUser().getEntitledAis().isEmpty());
|
||||
setVisible(!chatter.getEntitledAis().isEmpty());
|
||||
}
|
||||
|
||||
});
|
||||
@ -1287,6 +1289,10 @@ public abstract class LayoutPage extends BasePage {
|
||||
response.render(OnLoadHeaderItem.forScript("onedev.server.layout.onLoad();"));
|
||||
}
|
||||
|
||||
public ChatPanel getChatter() {
|
||||
return chatter;
|
||||
}
|
||||
|
||||
protected List<SidebarMenu> getSidebarMenus() {
|
||||
return Lists.newArrayList();
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.persistence.EntityNotFoundException;
|
||||
import javax.validation.ValidationException;
|
||||
|
||||
@ -31,14 +32,15 @@ import org.apache.wicket.request.cycle.RequestCycle;
|
||||
import org.apache.wicket.request.flow.RedirectToUrlException;
|
||||
import org.apache.wicket.request.mapper.parameter.PageParameters;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import io.onedev.server.ai.IssueHelper;
|
||||
import io.onedev.server.buildspecmodel.inputspec.InputContext;
|
||||
import io.onedev.server.data.migration.VersionedXmlDoc;
|
||||
import io.onedev.server.service.IssueLinkService;
|
||||
import io.onedev.server.service.IssueService;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.model.Issue;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
|
||||
@ -46,6 +48,10 @@ import io.onedev.server.search.entity.EntityQuery;
|
||||
import io.onedev.server.search.entity.issue.IssueQuery;
|
||||
import io.onedev.server.search.entity.issue.IssueQueryParseOption;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.service.IssueLinkService;
|
||||
import io.onedev.server.service.IssueService;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.service.support.ChatTool;
|
||||
import io.onedev.server.util.ProjectScope;
|
||||
import io.onedev.server.web.WebSession;
|
||||
import io.onedev.server.web.behavior.ChangeObserver;
|
||||
@ -65,12 +71,13 @@ import io.onedev.server.web.page.project.ProjectPage;
|
||||
import io.onedev.server.web.page.project.dashboard.ProjectDashboardPage;
|
||||
import io.onedev.server.web.page.project.issues.ProjectIssuesPage;
|
||||
import io.onedev.server.web.page.project.issues.list.ProjectIssueListPage;
|
||||
import io.onedev.server.web.util.ChatToolAware;
|
||||
import io.onedev.server.web.util.ConfirmClickModifier;
|
||||
import io.onedev.server.web.util.Cursor;
|
||||
import io.onedev.server.web.util.CursorSupport;
|
||||
import io.onedev.server.xodus.VisitInfoService;
|
||||
|
||||
public abstract class IssueDetailPage extends ProjectIssuesPage implements InputContext {
|
||||
public abstract class IssueDetailPage extends ProjectIssuesPage implements InputContext, ChatToolAware {
|
||||
|
||||
public static final String PARAM_ISSUE = "issue";
|
||||
|
||||
@ -78,6 +85,21 @@ public abstract class IssueDetailPage extends ProjectIssuesPage implements Input
|
||||
|
||||
protected final IModel<Issue> issueModel;
|
||||
|
||||
@Inject
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Inject
|
||||
private IssueService issueService;
|
||||
|
||||
@Inject
|
||||
private IssueLinkService issueLinkService;
|
||||
|
||||
@Inject
|
||||
private VisitInfoService visitInfoService;
|
||||
|
||||
@Inject
|
||||
private SettingService settingService;
|
||||
|
||||
public IssueDetailPage(PageParameters params) {
|
||||
super(params);
|
||||
|
||||
@ -98,11 +120,11 @@ public abstract class IssueDetailPage extends ProjectIssuesPage implements Input
|
||||
throw new ValidationException(MessageFormat.format(_T("Invalid issue number: {0}"), issueNumberString));
|
||||
}
|
||||
|
||||
Issue issue = getIssueService().find(getProject(), issueNumber);
|
||||
Issue issue = issueService.find(getProject(), issueNumber);
|
||||
if (issue == null) {
|
||||
throw new EntityNotFoundException(MessageFormat.format(_T("Unable to find issue #{0} in project {1}"), issueNumber, getProject()));
|
||||
} else {
|
||||
OneDev.getInstance(IssueLinkService.class).loadDeepLinks(issue);
|
||||
issueLinkService.loadDeepLinks(issue);
|
||||
if (!issue.getProject().equals(getProject()))
|
||||
throw new RestartResponseException(getPageClass(), paramsOf(issue));
|
||||
return issue;
|
||||
@ -293,7 +315,7 @@ public abstract class IssueDetailPage extends ProjectIssuesPage implements Input
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
getIssueService().delete(getIssue());
|
||||
issueService.delete(getIssue());
|
||||
var oldAuditContent = VersionedXmlDoc.fromBean(getIssue()).toXML();
|
||||
auditService.audit(getIssue().getProject(), "deleted issue \"" + getIssue().getReference().toString(getIssue().getProject()) + "\"", oldAuditContent, null);
|
||||
|
||||
@ -331,7 +353,7 @@ public abstract class IssueDetailPage extends ProjectIssuesPage implements Input
|
||||
|
||||
@Override
|
||||
protected List<Issue> query(EntityQuery<Issue> query, int offset, int count, ProjectScope projectScope) {
|
||||
return getIssueService().query(SecurityUtils.getSubject(), projectScope, query, false, offset, count);
|
||||
return issueService.query(SecurityUtils.getSubject(), projectScope, query, false, offset, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -362,13 +384,59 @@ public abstract class IssueDetailPage extends ProjectIssuesPage implements Input
|
||||
@Override
|
||||
public void onEndRequest(RequestCycle cycle) {
|
||||
if (SecurityUtils.getAuthUser() != null)
|
||||
OneDev.getInstance(VisitInfoService.class).visitIssue(SecurityUtils.getAuthUser(), getIssue());
|
||||
visitInfoService.visitIssue(SecurityUtils.getAuthUser(), getIssue());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatTool> getChatTools() {
|
||||
var issueId = getIssue().getId();
|
||||
return List.of(new ChatTool() {
|
||||
|
||||
@Override
|
||||
public ToolSpecification getSpecification() {
|
||||
return ToolSpecification.builder()
|
||||
.name("getCurrentIssue")
|
||||
.description("Get info of current issue in json format")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(JsonNode arguments) {
|
||||
var issue = issueService.load(issueId);
|
||||
try {
|
||||
return objectMapper.writeValueAsString(IssueHelper.getDetail(issue.getProject(), issue));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}, new ChatTool() {
|
||||
|
||||
@Override
|
||||
public ToolSpecification getSpecification() {
|
||||
return ToolSpecification.builder()
|
||||
.name("getCurrentIssueComments")
|
||||
.description("Get comments of current issue in json format")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(JsonNode arguments) {
|
||||
var issue = issueService.load(issueId);
|
||||
try {
|
||||
return objectMapper.writeValueAsString(IssueHelper.getComments(issue));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetach() {
|
||||
issueModel.detach();
|
||||
@ -392,11 +460,7 @@ public abstract class IssueDetailPage extends ProjectIssuesPage implements Input
|
||||
|
||||
@Override
|
||||
public FieldSpec getInputSpec(String inputName) {
|
||||
return OneDev.getInstance(SettingService.class).getIssueSetting().getFieldSpec(inputName);
|
||||
}
|
||||
|
||||
private IssueService getIssueService() {
|
||||
return OneDev.getInstance(IssueService.class);
|
||||
return settingService.getIssueSetting().getFieldSpec(inputName);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -17,9 +17,11 @@ import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.persistence.EntityNotFoundException;
|
||||
import javax.validation.ValidationException;
|
||||
|
||||
@ -64,21 +66,16 @@ import org.apache.wicket.request.mapper.parameter.PageParameters;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import io.onedev.server.ai.PullRequestHelper;
|
||||
import io.onedev.server.attachment.AttachmentSupport;
|
||||
import io.onedev.server.attachment.ProjectAttachmentSupport;
|
||||
import io.onedev.server.data.migration.VersionedXmlDoc;
|
||||
import io.onedev.server.service.PullRequestAssignmentService;
|
||||
import io.onedev.server.service.PullRequestChangeService;
|
||||
import io.onedev.server.service.PullRequestLabelService;
|
||||
import io.onedev.server.service.PullRequestService;
|
||||
import io.onedev.server.service.PullRequestReactionService;
|
||||
import io.onedev.server.service.PullRequestReviewService;
|
||||
import io.onedev.server.service.PullRequestWatchService;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.service.UserService;
|
||||
import io.onedev.server.entityreference.EntityReference;
|
||||
import io.onedev.server.entityreference.LinkTransformer;
|
||||
import io.onedev.server.git.GitUtils;
|
||||
@ -106,6 +103,16 @@ import io.onedev.server.search.entity.pullrequest.PullRequestQuery;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.security.permission.ProjectPermission;
|
||||
import io.onedev.server.security.permission.ReadCode;
|
||||
import io.onedev.server.service.PullRequestAssignmentService;
|
||||
import io.onedev.server.service.PullRequestChangeService;
|
||||
import io.onedev.server.service.PullRequestLabelService;
|
||||
import io.onedev.server.service.PullRequestReactionService;
|
||||
import io.onedev.server.service.PullRequestReviewService;
|
||||
import io.onedev.server.service.PullRequestService;
|
||||
import io.onedev.server.service.PullRequestWatchService;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.service.UserService;
|
||||
import io.onedev.server.service.support.ChatTool;
|
||||
import io.onedev.server.util.DateUtils;
|
||||
import io.onedev.server.util.ProjectScope;
|
||||
import io.onedev.server.web.WebSession;
|
||||
@ -159,6 +166,7 @@ import io.onedev.server.web.page.project.pullrequests.detail.changes.PullRequest
|
||||
import io.onedev.server.web.page.project.pullrequests.detail.codecomments.PullRequestCodeCommentsPage;
|
||||
import io.onedev.server.web.page.project.pullrequests.detail.operationdlg.MergePullRequestOptionPanel;
|
||||
import io.onedev.server.web.page.project.pullrequests.detail.operationdlg.OperationCommentPanel;
|
||||
import io.onedev.server.web.util.ChatToolAware;
|
||||
import io.onedev.server.web.util.ConfirmClickModifier;
|
||||
import io.onedev.server.web.util.Cursor;
|
||||
import io.onedev.server.web.util.CursorSupport;
|
||||
@ -169,7 +177,7 @@ import io.onedev.server.web.util.editbean.CommitMessageBean;
|
||||
import io.onedev.server.web.util.editbean.LabelsBean;
|
||||
import io.onedev.server.xodus.VisitInfoService;
|
||||
|
||||
public abstract class PullRequestDetailPage extends ProjectPage implements PullRequestAware {
|
||||
public abstract class PullRequestDetailPage extends ProjectPage implements PullRequestAware, ChatToolAware {
|
||||
|
||||
public static final String PARAM_REQUEST = "request";
|
||||
|
||||
@ -185,6 +193,45 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
|
||||
private MergeStrategy mergeStrategy;
|
||||
|
||||
@Inject
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Inject
|
||||
private PullRequestService pullRequestService;
|
||||
|
||||
@Inject
|
||||
private GitService gitService;
|
||||
|
||||
@Inject
|
||||
private PullRequestReviewService pullRequestReviewService;
|
||||
|
||||
@Inject
|
||||
private PullRequestChangeService pullRequestChangeService;
|
||||
|
||||
@Inject
|
||||
private PullRequestAssignmentService pullRequestAssignmentService;
|
||||
|
||||
@Inject
|
||||
private PullRequestLabelService pullRequestLabelService;
|
||||
|
||||
@Inject
|
||||
private PullRequestWatchService pullRequestWatchService;
|
||||
|
||||
@Inject
|
||||
private PullRequestReactionService pullRequestReactionService;
|
||||
|
||||
@Inject
|
||||
private UserService userService;
|
||||
|
||||
@Inject
|
||||
private SettingService settingService;
|
||||
|
||||
@Inject
|
||||
private VisitInfoService visitInfoService;
|
||||
|
||||
@Inject
|
||||
private Set<PullRequestSummaryContribution> summaryContributions;
|
||||
|
||||
public PullRequestDetailPage(PageParameters params) {
|
||||
super(params);
|
||||
|
||||
@ -205,7 +252,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
throw new ValidationException(MessageFormat.format(_T("Invalid pull request number: {0}"), requestNumberString));
|
||||
}
|
||||
|
||||
PullRequest request = getPullRequestService().find(getProject(), requestNumber);
|
||||
PullRequest request = pullRequestService.find(getProject(), requestNumber);
|
||||
if (request == null) {
|
||||
throw new EntityNotFoundException(MessageFormat.format(_T("Unable to find pull request #{0} in project {1}"), requestNumber, getProject().getPath()));
|
||||
} else if (!request.getTargetProject().equals(getProject())) {
|
||||
@ -225,22 +272,6 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
latestUpdateId = requestModel.getObject().getLatestUpdate().getId();
|
||||
}
|
||||
|
||||
private GitService getGitService() {
|
||||
return OneDev.getInstance(GitService.class);
|
||||
}
|
||||
|
||||
private PullRequestService getPullRequestService() {
|
||||
return OneDev.getInstance(PullRequestService.class);
|
||||
}
|
||||
|
||||
private PullRequestReviewService getPullRequestReviewService() {
|
||||
return OneDev.getInstance(PullRequestReviewService.class);
|
||||
}
|
||||
|
||||
private PullRequestChangeService getPullRequestChangeService() {
|
||||
return OneDev.getInstance(PullRequestChangeService.class);
|
||||
}
|
||||
|
||||
private WebMarkupContainer newRequestHead() {
|
||||
WebMarkupContainer requestHead = new WebMarkupContainer("requestHeader");
|
||||
requestHead.setOutputMarkupId(true);
|
||||
@ -340,7 +371,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
super.onSubmit(target, form);
|
||||
|
||||
var user = SecurityUtils.getUser();
|
||||
OneDev.getInstance(PullRequestChangeService.class).changeTitle(user, getPullRequest(), title);
|
||||
pullRequestChangeService.changeTitle(user, getPullRequest(), title);
|
||||
notifyPullRequestChange(target);
|
||||
isEditingTitle = false;
|
||||
|
||||
@ -919,7 +950,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
public void onEndRequest(RequestCycle cycle) {
|
||||
if (SecurityUtils.getAuthUser() != null)
|
||||
OneDev.getInstance(VisitInfoService.class).visitPullRequest(SecurityUtils.getAuthUser(), getPullRequest());
|
||||
visitInfoService.visitPullRequest(SecurityUtils.getAuthUser(), getPullRequest());
|
||||
}
|
||||
|
||||
});
|
||||
@ -980,7 +1011,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
public void onClick(AjaxRequestTarget target) {
|
||||
var user = SecurityUtils.getUser();
|
||||
OneDev.getInstance(PullRequestChangeService.class).changeTargetBranch(user, getPullRequest(), branch);
|
||||
pullRequestChangeService.changeTargetBranch(user, getPullRequest(), branch);
|
||||
notifyPullRequestChange(target);
|
||||
dropdown.close();
|
||||
}
|
||||
@ -1087,7 +1118,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
var assignment = new PullRequestAssignment();
|
||||
assignment.setRequest(getPullRequest());
|
||||
assignment.setUser(SecurityUtils.getUser());
|
||||
OneDev.getInstance(PullRequestAssignmentService.class).create(assignment);
|
||||
pullRequestAssignmentService.create(assignment);
|
||||
((BasePage)getPage()).notifyObservableChange(target,
|
||||
PullRequest.getChangeObservable(getPullRequest().getId()));
|
||||
}
|
||||
@ -1137,7 +1168,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
protected void onUpdated(IPartialPageRequestHandler handler, Serializable bean, String propertyName) {
|
||||
LabelsBean labelsBean = (LabelsBean) bean;
|
||||
OneDev.getInstance(PullRequestLabelService.class).sync(getPullRequest(), labelsBean.getLabels());
|
||||
pullRequestLabelService.sync(getPullRequest(), labelsBean.getLabels());
|
||||
handler.add(labelsContainer);
|
||||
}
|
||||
|
||||
@ -1184,12 +1215,12 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
|
||||
@Override
|
||||
protected void onSaveWatch(EntityWatch watch) {
|
||||
OneDev.getInstance(PullRequestWatchService.class).createOrUpdate((PullRequestWatch) watch);
|
||||
pullRequestWatchService.createOrUpdate((PullRequestWatch) watch);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleteWatch(EntityWatch watch) {
|
||||
OneDev.getInstance(PullRequestWatchService.class).delete((PullRequestWatch) watch);
|
||||
pullRequestWatchService.delete((PullRequestWatch) watch);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1217,7 +1248,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
|
||||
@Override
|
||||
public void onClick(AjaxRequestTarget target) {
|
||||
getPullRequestService().checkAsync(getPullRequest(), false, true);
|
||||
pullRequestService.checkAsync(getPullRequest(), false, true);
|
||||
Session.get().success(_T("Pull request synchronization submitted"));
|
||||
}
|
||||
|
||||
@ -1227,7 +1258,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
public void onClick() {
|
||||
PullRequest request = getPullRequest();
|
||||
getPullRequestService().delete(request);
|
||||
pullRequestService.delete(request);
|
||||
var oldAuditContent = VersionedXmlDoc.fromBean(request).toXML();
|
||||
auditService.audit(request.getProject(), "deleted pull request \"" + request.getReference().toString(request.getProject()) + "\"", oldAuditContent, null);
|
||||
|
||||
@ -1278,7 +1309,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
protected List<PullRequest> query(EntityQuery<PullRequest> query,
|
||||
int offset, int count, ProjectScope projectScope) {
|
||||
return getPullRequestService().query(SecurityUtils.getSubject(), projectScope!=null?projectScope.getProject():null, query, false, offset, count);
|
||||
return pullRequestService.query(SecurityUtils.getSubject(), projectScope!=null?projectScope.getProject():null, query, false, offset, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1347,7 +1378,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
protected void onUpdate(AjaxRequestTarget target) {
|
||||
var user = SecurityUtils.getUser();
|
||||
OneDev.getInstance(PullRequestChangeService.class).changeMergeStrategy(user, getPullRequest(), mergeStrategy);
|
||||
pullRequestChangeService.changeMergeStrategy(user, getPullRequest(), mergeStrategy);
|
||||
notifyPullRequestChange(target);
|
||||
}
|
||||
|
||||
@ -1420,7 +1451,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
protected String onSave(AjaxRequestTarget target, CommitMessageBean bean) {
|
||||
var request = getPullRequest();
|
||||
var system = OneDev.getInstance(UserService.class).getSystem();
|
||||
var system = userService.getSystem();
|
||||
var branchProtection = getProject().getBranchProtection(request.getTargetBranch(), system);
|
||||
var errorMessage = branchProtection.checkCommitMessage(bean.getCommitMessage(),
|
||||
request.getMergeStrategy() != SQUASH_SOURCE_BRANCH_COMMITS);
|
||||
@ -1429,10 +1460,10 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
} else {
|
||||
autoMerge.setCommitMessage(bean.getCommitMessage());
|
||||
var user = SecurityUtils.getUser();
|
||||
getPullRequestChangeService().changeAutoMerge(user, request, autoMerge);
|
||||
pullRequestChangeService.changeAutoMerge(user, request, autoMerge);
|
||||
Session.get().success(_T("Preset commit message updated"));
|
||||
close();
|
||||
getPullRequestService().checkAutoMerge(request);
|
||||
pullRequestService.checkAutoMerge(request);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -1530,7 +1561,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
var autoMerge = new AutoMerge();
|
||||
autoMerge.setEnabled(true);
|
||||
autoMerge.setCommitMessage(bean.getCommitMessage());
|
||||
getPullRequestChangeService().changeAutoMerge(user, getPullRequest(), autoMerge);
|
||||
pullRequestChangeService.changeAutoMerge(user, getPullRequest(), autoMerge);
|
||||
target.add(autoMergeContainer);
|
||||
close();
|
||||
return null;
|
||||
@ -1552,13 +1583,13 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
var autoMerge = new AutoMerge();
|
||||
autoMerge.setEnabled(true);
|
||||
autoMerge.setCommitMessage(request.getAutoMerge().getCommitMessage());
|
||||
getPullRequestChangeService().changeAutoMerge(user, request, autoMerge);
|
||||
pullRequestChangeService.changeAutoMerge(user, request, autoMerge);
|
||||
target.add(autoMergeContainer);
|
||||
}
|
||||
} else {
|
||||
var autoMerge = new AutoMerge();
|
||||
autoMerge.setCommitMessage(request.getAutoMerge().getCommitMessage());
|
||||
getPullRequestChangeService().changeAutoMerge(user, request, autoMerge);
|
||||
pullRequestChangeService.changeAutoMerge(user, request, autoMerge);
|
||||
target.add(autoMergeContainer);
|
||||
}
|
||||
}
|
||||
@ -1683,7 +1714,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
protected void onSaveComment(AjaxRequestTarget target, String comment) {
|
||||
var user = SecurityUtils.getUser();
|
||||
OneDev.getInstance(PullRequestChangeService.class).changeDescription(user, getPullRequest(), comment);
|
||||
pullRequestChangeService.changeDescription(user, getPullRequest(), comment);
|
||||
((BasePage)getPage()).notifyObservableChange(target,
|
||||
PullRequest.getChangeObservable(getPullRequest().getId()));
|
||||
}
|
||||
@ -1753,10 +1784,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
|
||||
@Override
|
||||
public void onToggleEmoji(AjaxRequestTarget target, String emoji) {
|
||||
OneDev.getInstance(PullRequestReactionService.class).toggleEmoji(
|
||||
SecurityUtils.getUser(),
|
||||
getPullRequest(),
|
||||
emoji);
|
||||
pullRequestReactionService.toggleEmoji(SecurityUtils.getUser(), getPullRequest(), emoji);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1799,8 +1827,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
|
||||
@Override
|
||||
protected List<PullRequestSummaryPart> load() {
|
||||
List<PullRequestSummaryContribution> contributions =
|
||||
new ArrayList<>(OneDev.getExtensions(PullRequestSummaryContribution.class));
|
||||
List<PullRequestSummaryContribution> contributions = new ArrayList<>(summaryContributions);
|
||||
contributions.sort(Comparator.comparing(PullRequestSummaryContribution::getOrder));
|
||||
|
||||
List<PullRequestSummaryPart> parts = new ArrayList<>();
|
||||
@ -1889,7 +1916,6 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
return errorMessage;
|
||||
if (targetHead.equals(request.getTarget().getObjectId())
|
||||
&& sourceHead.equals(request.getSourceHead())) {
|
||||
var gitService = OneDev.getInstance(GitService.class);
|
||||
var amendedCommitId = gitService.amendCommit(request.getProject(), mergeCommit.copy(),
|
||||
mergeCommit.getAuthorIdent(), mergeCommit.getCommitterIdent(),
|
||||
bean.getCommitMessage());
|
||||
@ -2001,7 +2027,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
protected String operate(AjaxRequestTarget target) {
|
||||
if (canOperate()) {
|
||||
getPullRequestReviewService().review(SecurityUtils.getUser(), getPullRequest(), true, getComment());
|
||||
pullRequestReviewService.review(SecurityUtils.getUser(), getPullRequest(), true, getComment());
|
||||
notifyPullRequestChange(target);
|
||||
Session.get().success(_T("Approved"));
|
||||
return null;
|
||||
@ -2044,7 +2070,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
protected String operate(AjaxRequestTarget target) {
|
||||
if (canOperate()) {
|
||||
getPullRequestReviewService().review(SecurityUtils.getUser(), getPullRequest(), false, getComment());
|
||||
pullRequestReviewService.review(SecurityUtils.getUser(), getPullRequest(), false, getComment());
|
||||
notifyPullRequestChange(target);
|
||||
Session.get().success(_T("Requested For changes"));
|
||||
return null;
|
||||
@ -2091,7 +2117,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
if (errorMessage != null)
|
||||
return errorMessage;
|
||||
}
|
||||
getPullRequestService().merge(SecurityUtils.getUser(), getPullRequest(), commitMessage);
|
||||
pullRequestService.merge(SecurityUtils.getUser(), getPullRequest(), commitMessage);
|
||||
notifyPullRequestChange(target);
|
||||
return null;
|
||||
} else {
|
||||
@ -2123,7 +2149,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
protected String operate(AjaxRequestTarget target) {
|
||||
if (canOperate()) {
|
||||
getPullRequestService().discard(SecurityUtils.getUser(), getPullRequest(), getComment());
|
||||
pullRequestService.discard(SecurityUtils.getUser(), getPullRequest(), getComment());
|
||||
notifyPullRequestChange(target);
|
||||
return null;
|
||||
} else {
|
||||
@ -2160,7 +2186,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
protected String operate(AjaxRequestTarget target) {
|
||||
if (canOperate()) {
|
||||
getPullRequestService().reopen(SecurityUtils.getUser(), getPullRequest(), getComment());
|
||||
pullRequestService.reopen(SecurityUtils.getUser(), getPullRequest(), getComment());
|
||||
notifyPullRequestChange(target);
|
||||
return null;
|
||||
} else {
|
||||
@ -2199,7 +2225,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
protected String operate(AjaxRequestTarget target) {
|
||||
if (canOperate()) {
|
||||
getPullRequestService().deleteSourceBranch(SecurityUtils.getUser(), getPullRequest(), getComment());
|
||||
pullRequestService.deleteSourceBranch(SecurityUtils.getUser(), getPullRequest(), getComment());
|
||||
notifyPullRequestChange(target);
|
||||
Session.get().success(_T("Deleted source branch"));
|
||||
return null;
|
||||
@ -2239,7 +2265,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
@Override
|
||||
protected String operate(AjaxRequestTarget target) {
|
||||
if (canOperate()) {
|
||||
getPullRequestService().restoreSourceBranch(SecurityUtils.getUser(), getPullRequest(), getComment());
|
||||
pullRequestService.restoreSourceBranch(SecurityUtils.getUser(), getPullRequest(), getComment());
|
||||
notifyPullRequestChange(target);
|
||||
Session.get().success(_T("Restored source branch"));
|
||||
return null;
|
||||
@ -2274,7 +2300,6 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
ObjectId targetHead = request.getTarget().getObjectId();
|
||||
ObjectId mergeCommitId;
|
||||
|
||||
var gitService = getGitService();
|
||||
if (updateByMerge) {
|
||||
String commitMessage;
|
||||
if (!request.getSourceProject().equals(project)) {
|
||||
@ -2300,8 +2325,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
&& !project.hasValidCommitSignature(project.getRevCommit(targetHead, true))) {
|
||||
return _T("No valid signature for head commit of target branch");
|
||||
}
|
||||
if (protection.isCommitSignatureRequired()
|
||||
&& OneDev.getInstance(SettingService.class).getGpgSetting().getSigningKey() == null) {
|
||||
if (protection.isCommitSignatureRequired() && settingService.getGpgSetting().getSigningKey() == null) {
|
||||
return _T("Commit signature required but no GPG signing key specified");
|
||||
}
|
||||
var error = gitService.checkCommitMessages(protection, project, sourceHead, mergeCommitId, new HashMap<>());
|
||||
@ -2313,11 +2337,10 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
|
||||
private void updateSourceBranch(ObjectId commitId) {
|
||||
var request = getPullRequest();
|
||||
var gitService = getGitService();
|
||||
if (!request.getSourceProject().equals(request.getTargetProject()))
|
||||
gitService.fetch(request.getSourceProject(), request.getTargetProject(), commitId.name());
|
||||
var oldCommitId = request.getSourceHead();
|
||||
getGitService().updateRef(request.getSourceProject(), request.getSourceRef(), commitId, oldCommitId);
|
||||
gitService.updateRef(request.getSourceProject(), request.getSourceRef(), commitId, oldCommitId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -2326,6 +2349,52 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatTool> getChatTools() {
|
||||
var pullRequestId = getPullRequest().getId();
|
||||
return List.of(new ChatTool() {
|
||||
|
||||
@Override
|
||||
public ToolSpecification getSpecification() {
|
||||
return ToolSpecification.builder()
|
||||
.name("getCurrentPullRequest")
|
||||
.description("Get info of current pull request in json format")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(JsonNode arguments) {
|
||||
var pullRequest = pullRequestService.load(pullRequestId);
|
||||
try {
|
||||
return objectMapper.writeValueAsString(PullRequestHelper.getDetail(pullRequest.getProject(), pullRequest));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}, new ChatTool() {
|
||||
|
||||
@Override
|
||||
public ToolSpecification getSpecification() {
|
||||
return ToolSpecification.builder()
|
||||
.name("getCurrentPullRequestComments")
|
||||
.description("Get comments of current pull request in json format")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(JsonNode arguments) {
|
||||
var pullRequest = pullRequestService.load(pullRequestId);
|
||||
try {
|
||||
return objectMapper.writeValueAsString(PullRequestHelper.getComments(pullRequest));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public static PageParameters paramsOf(PullRequest pullRequest) {
|
||||
return paramsOf(pullRequest.getProject(), pullRequest.getNumber());
|
||||
}
|
||||
|
||||
@ -22,10 +22,11 @@
|
||||
</div>
|
||||
</wicket:fragment>
|
||||
<wicket:fragment wicket:id="optionsFrag">
|
||||
<div class="pull-request-activity-options">
|
||||
<a wicket:id="showComments" t:data-tippy-content="Toggle comments" class="btn btn-xs text-muted btn-active-secondary btn-icon"><wicket:svg href="comments" class="icon"></wicket:svg></a>
|
||||
<a wicket:id="showCommits" t:data-tippy-content="Toggle commits" class="btn btn-xs text-muted btn-active-secondary btn-icon"><wicket:svg href="commit" class="icon"></wicket:svg></a>
|
||||
<a wicket:id="showChangeHistory" t:data-tippy-content="Toggle change history" class="btn btn-xs text-muted btn-active-secondary btn-icon"><wicket:svg href="history" class="icon"></wicket:svg></a>
|
||||
<a wicket:id="aiSummarize" t:data-tippy-content="Summarize comments with AI" class="btn btn-xs btn-outline-secondary btn-hover-primary btn-icon"><wicket:svg href="ai-summarize" class="icon"></wicket:svg></a>
|
||||
<div class="btn-group">
|
||||
<a wicket:id="showComments" t:data-tippy-content="Toggle comments" class="btn btn-xs text-muted btn-outline-secondary btn-active-secondary btn-icon"><wicket:svg href="comments" class="icon"></wicket:svg></a>
|
||||
<a wicket:id="showCommits" t:data-tippy-content="Toggle commits" class="btn btn-xs text-muted btn-outline-secondary btn-active-secondary btn-icon"><wicket:svg href="commit" class="icon"></wicket:svg></a>
|
||||
<a wicket:id="showChangeHistory" t:data-tippy-content="Toggle change history" class="btn btn-xs text-muted btn-outline-secondary btn-active-secondary btn-icon"><wicket:svg href="history" class="icon"></wicket:svg></a>
|
||||
</div>
|
||||
</wicket:fragment>
|
||||
</wicket:extend>
|
||||
@ -61,6 +61,7 @@ import io.onedev.server.web.ajaxlistener.ConfirmLeaveListener;
|
||||
import io.onedev.server.web.behavior.ChangeObserver;
|
||||
import io.onedev.server.web.component.comment.CommentInput;
|
||||
import io.onedev.server.web.page.base.BasePage;
|
||||
import io.onedev.server.web.page.layout.LayoutPage;
|
||||
import io.onedev.server.web.page.project.pullrequests.detail.PullRequestDetailPage;
|
||||
import io.onedev.server.web.page.project.pullrequests.detail.activities.activity.PullRequestChangeActivity;
|
||||
import io.onedev.server.web.page.project.pullrequests.detail.activities.activity.PullRequestCommentActivity;
|
||||
@ -409,6 +410,24 @@ public class PullRequestActivitiesPage extends PullRequestDetailPage {
|
||||
|
||||
public Component renderOptions(String componentId) {
|
||||
Fragment fragment = new Fragment(componentId, "optionsFrag", this);
|
||||
|
||||
fragment.add(new AjaxLink<Void>("aiSummarize") {
|
||||
|
||||
@Override
|
||||
public void onClick(AjaxRequestTarget target) {
|
||||
var page = (LayoutPage)getPage();
|
||||
page.getChatter().show(target, "Summarize comments of current pull request. Display summary in " + getSession().getLocale().getDisplayLanguage());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigure() {
|
||||
super.onConfigure();
|
||||
var page = (LayoutPage)getPage();
|
||||
setVisible(!page.getChatter().getEntitledAis().isEmpty());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
fragment.add(showCommentsLink = new AjaxLink<Void>("showComments") {
|
||||
|
||||
@Override
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
package io.onedev.server.web.util;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.onedev.server.service.support.ChatTool;
|
||||
|
||||
public interface ChatToolAware {
|
||||
|
||||
List<ChatTool> getChatTools();
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user