Skip to content

Commit

Permalink
Add support for adding and editing answers
Browse files Browse the repository at this point in the history
  • Loading branch information
mythz committed Mar 28, 2024
1 parent 6414048 commit 0bf742d
Show file tree
Hide file tree
Showing 12 changed files with 685 additions and 157 deletions.
93 changes: 93 additions & 0 deletions MyApp.ServiceInterface/Data/QuestionsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public class QuestionsProvider(ILogger<QuestionsProvider> log, IMessageProducer
{
public const int MostVotedScore = 10;
public const int AcceptedScore = 9;
public static List<string> ModelUserNames { get; } = [
"phi", "gemma-2b", "qwen-4b", "codellama", "gemma", "deepseek-coder", "mistral", "mixtral"
];
public static Dictionary<string,int> ModelScores = new()
{
["phi"] = 1, //2.7B
Expand Down Expand Up @@ -99,6 +102,16 @@ await Task.WhenAll(
r2.WriteFileAsync(virtualPath, contents),
fs.WriteFileAsync(virtualPath, contents));
}

public async Task DeleteFileAsync(string virtualPath)
{
fs.DeleteFile(virtualPath);
await r2.AmazonS3.DeleteObjectAsync(new Amazon.S3.Model.DeleteObjectRequest
{
BucketName = r2.BucketName,
Key = r2.SanitizePath(virtualPath),
});
}

public async Task WriteMetaAsync(Meta meta)
{
Expand Down Expand Up @@ -136,6 +149,31 @@ public async Task<QuestionFiles> GetQuestionAsync(int id)
return questionFiles;
}

public async Task<IVirtualFile?> GetAnswerFileAsync(string refId)
{
if (refId.IndexOf('-') < 0)
throw new ArgumentException("Invalid Answer Id", nameof(refId));

var postId = refId.LeftPart('-').ToInt();
var userName = refId.RightPart('-');
var answerPath = ModelUserNames.Contains(userName)
? GetModelAnswerPath(postId, userName)
: GetHumanAnswerPath(postId, userName);

var file = fs.GetFile(answerPath)
?? r2.GetFile(answerPath);

if (file == null)
{
// After first edit AI Model is converted to h. (Post) answer
var modelAnswerPath = GetHumanAnswerPath(postId, userName);
file = fs.GetFile(modelAnswerPath)
?? r2.GetFile(modelAnswerPath);
}

return file;
}

public async Task SaveQuestionAsync(Post post)
{
await SaveFileAsync(GetQuestionPath(post.Id), ToJson(post));
Expand Down Expand Up @@ -168,6 +206,61 @@ public async Task SaveRemoteFileAsync(string virtualPath, string contents)
{
await r2.WriteFileAsync(virtualPath, contents);
}

public async Task SaveAnswerEditAsync(IVirtualFile existingAnswer, string userName, string body, string editReason)
{
var now = DateTime.UtcNow;
var existingAnswerJson = await existingAnswer.ReadAllTextAsync();
var tasks = new List<Task>();

var fileName = existingAnswer.VirtualPath.TrimStart('/').Replace("/", "");
var postId = fileName.LeftPart('.').ToInt();
string existingAnswerBy = "";
var newAnswer = new Post
{
Id = postId,
};

if (fileName.Contains(".a."))
{
existingAnswerBy = fileName.RightPart(".a.").LastLeftPart('.');
var datePart = DateTime.UtcNow.ToString("yyMMdd-HHmmss");
var editFilePath = existingAnswer.VirtualPath.LastLeftPart('/') + "/edit.a." + postId + "-" + userName + "_" + datePart + ".json";
tasks.Add(SaveFileAsync(editFilePath, existingAnswerJson));
tasks.Add(DeleteFileAsync(existingAnswer.VirtualPath));

var obj = (Dictionary<string,object>)JSON.parse(existingAnswerJson);
newAnswer.CreationDate = obj.TryGetValue("created", out var oCreated) && oCreated is int created
? DateTimeOffset.FromUnixTimeSeconds(created).DateTime
: existingAnswer.LastModified;
}
else if (fileName.Contains(".h."))
{
existingAnswerBy = fileName.RightPart(".h.").LastLeftPart('.');
var datePart = DateTime.UtcNow.ToString("yyMMdd-HHmmss");
newAnswer = existingAnswerJson.FromJson<Post>();

// Just override the existing answer if it's the same user
if (newAnswer.ModifiedBy != userName)
{
var editFilePath = existingAnswer.VirtualPath.LastLeftPart('/') + "/edit.h." + postId + "-" + userName + "_" + datePart + ".json";
tasks.Add(SaveFileAsync(editFilePath, existingAnswerJson));
}
}
else throw new ArgumentException($"Invalid Answer File {existingAnswer.Name}", nameof(existingAnswer));

newAnswer.Body = body;
newAnswer.CreatedBy ??= existingAnswerBy;
newAnswer.ModifiedBy = userName;
newAnswer.LastEditDate = now;
newAnswer.ModifiedReason = editReason;

var newFileName = $"{existingAnswer.Name.LeftPart('.')}.h.{existingAnswerBy}.json";
var newFilePath = existingAnswer.VirtualPath.LastLeftPart('/') + "/" + newFileName;
tasks.Add(SaveFileAsync(newFilePath, ToJson(newAnswer)));

await Task.WhenAll(tasks);
}

public async Task DeleteQuestionFilesAsync(int id)
{
Expand Down
49 changes: 49 additions & 0 deletions MyApp.ServiceInterface/QuestionServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public async Task<object> Any(AskQuestion request)
};
}

public async Task<object> Any(EditQuestion request)
{
return new EditQuestionResponse();
}

public async Task<EmptyResponse> Any(DeleteQuestion request)
{
await questions.DeleteQuestionFilesAsync(request.Id);
Expand Down Expand Up @@ -99,6 +104,27 @@ public async Task<object> Any(AnswerQuestion request)
return new AnswerQuestionResponse();
}

/* /100/000
* 001.a.model.json <OpenAI>
* Edit 1:
* 001.h.model.json <Post>
* edit.a.001-model_20240301-1200.json // original model answer, Modified Date <OpenAI>
* Edit 2:
* 001.h.model.json <Post> #2
* edit.a.001-model_20240301-130303.json // #1 edit model answer, Modified Date <Post>
* edit.a.001-model_20240301-120101.json // #0 original model answer, Modified Date <OpenAI>
*/
public async Task<object> Any(EditAnswer request)
{
var answerFile = await questions.GetAnswerFileAsync(request.Id);
if (answerFile == null)
throw HttpError.NotFound("Answer does not exist");

await questions.SaveAnswerEditAsync(answerFile, GetUserName(), request.Body, request.EditReason);

return new EditAnswerResponse();
}

public async Task<object> Any(GetQuestionFile request)
{
var questionFiles = await questions.GetQuestionFilesAsync(request.Id);
Expand All @@ -108,6 +134,29 @@ public async Task<object> Any(GetQuestionFile request)
return new HttpResult(file, MimeTypes.Json);
}

public async Task<object> Any(GetAnswerBody request)
{
var answerFile = await questions.GetAnswerFileAsync(request.Id);
if (answerFile == null)
throw HttpError.NotFound("Answer does not exist");

var json = await answerFile.ReadAllTextAsync();
if (answerFile.Name.Contains(".a."))
{
var obj = (Dictionary<string,object>)JSON.parse(json);
var choices = (List<object>) obj["choices"];
var choice = (Dictionary<string,object>)choices[0];
var message = (Dictionary<string,object>)choice["message"];
var body = (string)message["content"];
return new HttpResult(body, MimeTypes.PlainText);
}
else
{
var answer = json.FromJson<Post>();
return new HttpResult(answer.Body, MimeTypes.PlainText);
}
}

public async Task Any(CreateWorkerAnswer request)
{
var json = request.Json;
Expand Down
56 changes: 53 additions & 3 deletions MyApp.ServiceModel/Posts.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Complete declarative AutoQuery services for Bookings CRUD example:
// https://docs.servicestack.net/autoquery-crud-bookings

using System.ComponentModel.DataAnnotations;
using ServiceStack;
using ServiceStack.DataAnnotations;

Expand Down Expand Up @@ -54,7 +53,13 @@ public class Post

public string? RefId { get; set; }

[Ignore] public string? Body { get; set; }
public string? Body { get; set; }

public string? ModifiedReason { get; set; }

public DateTime? LockedDate { get; set; }

public string? LockedReason { get; set; }
}

public class PostJob
Expand Down Expand Up @@ -271,7 +276,6 @@ public class AskQuestion : IPost, IReturn<AskQuestionResponse>
[Input(Type="hidden")]
public string? RefId { get; set; }
}

public class AskQuestionResponse
{
public int Id { get; set; }
Expand All @@ -280,6 +284,26 @@ public class AskQuestionResponse
public ResponseStatus? ResponseStatus { get; set; }
}

[ValidateIsAuthenticated]
public class EditQuestion : IPost, IReturn<EditQuestionResponse>
{
[ValidateNotEmpty, ValidateMinimumLength(20), ValidateMaximumLength(120)]
[Input(Type = "text", Help = "A summary of what your main question is asking"), FieldCss(Field="col-span-12")]
public required string Title { get; set; }

[ValidateNotEmpty, ValidateMinimumLength(30), ValidateMaximumLength(32768)]
[Input(Type="MarkdownInput", Help = "Include all information required for someone to identity and resolve your exact question"), FieldCss(Field="col-span-12", Input="h-60")]
public required string Body { get; set; }

[ValidateNotEmpty, ValidateMinimumLength(2, Message = "At least 1 tag required"), ValidateMaximumLength(120)]
[Input(Type = "tag", Help = "Up to 5 tags relevant to your question"), FieldCss(Field="col-span-12")]
public required List<string> Tags { get; set; }
}
public class EditQuestionResponse
{
public ResponseStatus? ResponseStatus { get; set; }
}

[ValidateIsAuthenticated]
[Description("Your Answer")]
public class AnswerQuestion : IPost, IReturn<AnswerQuestionResponse>
Expand All @@ -300,11 +324,37 @@ public class AnswerQuestionResponse
public ResponseStatus? ResponseStatus { get; set; }
}

[ValidateIsAuthenticated]
[Description("Your Answer")]
public class EditAnswer : IPost, IReturn<EditAnswerResponse>
{
[Input(Type="hidden")]
[ValidateNotEmpty]
public required string Id { get; set; }

[ValidateNotEmpty, ValidateMinimumLength(30), ValidateMaximumLength(32768)]
[Input(Type="MarkdownInput", Label=""), FieldCss(Field="col-span-12", Input="h-60")]
public required string Body { get; set; }

[Input(Type="text", Placeholder="Short summary of this edit (e.g. corrected spelling, grammar, improved formatting)"),FieldCss(Field = "col-span-12")]
[ValidateNotEmpty, ValidateMinimumLength(4)]
public required string EditReason { get; set; }
}
public class EditAnswerResponse
{
public ResponseStatus? ResponseStatus { get; set; }
}

public class PreviewMarkdown : IPost, IReturn<string>
{
public string Markdown { get; set; }
}

public class GetAnswerBody : IGet, IReturn<string>
{
public string Id { get; set; }
}

[ValidateHasRole(Roles.Moderator)]
public class DeleteQuestion : IGet, IReturn<EmptyResponse>
{
Expand Down
2 changes: 1 addition & 1 deletion MyApp/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
})
</script>
<script src="lib/js/highlight.js"></script>
<script>hljs.highlightAll()</script>
<script src="lib/js/default.js"></script>
</body>

</html>
9 changes: 9 additions & 0 deletions MyApp/Components/Pages/Questions/LiveAnswers.razor
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@
</div>
</div>
}
else
{
<div class="float-right">
<a href=@QuestionPath class="whitespace-nowrap font-medium text-indigo-700 dark:text-indigo-300 hover:text-indigo-500">
full question page
<span aria-hidden="true"> &rarr;</span>
</a>
</div>
}

<div id="answers" class="mt-8">
@if (question.Answers.Count > 0)
Expand Down
27 changes: 17 additions & 10 deletions MyApp/Components/Shared/QuestionPost.razor
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,21 @@
{
<span>asked</span>
}
<b>@((DateTime.UtcNow - Question.Post.CreationDate).TimeAgo())</b>
<b class="ml-1">@((DateTime.UtcNow - Question.Post.CreationDate).TimeAgo())</b>
</div>
@if (Question.Post.LastEditDate != null)
{
<div>
<b>@((DateTime.UtcNow - Question.Post.LastEditDate.Value).TimeAgo())</b>
</div>
}
<div>
<span>viewed</span>
<b>@($"{Question.ViewCount:n0}") times</b>
</div>
@if (Question.ViewCount > 1)
{
<div>
<span>viewed</span>
<b>@($"{Question.ViewCount:n0}") times</b>
</div>
}
</div>
@if (Question.Post.CreatedBy != null)
{
Expand Down Expand Up @@ -103,7 +106,7 @@
</article>

<div id="answers" class="mt-16">
@if (@Question.Answers.Count > 0)
@if (Question.Answers.Count > 0)
{
<h3 class="text-2xl font-semibold">
@Question.Answers.Count Answers
Expand All @@ -112,7 +115,7 @@
<div>
@foreach (var answer in Question.Answers)
{
<div class="py-8 border-b border-gray-200 dark:border-gray-700">
<div data-answer=@answer.Id class="py-8 border-b border-gray-200 dark:border-gray-700">
<div class="flex">
<div class="md:w-32 pr-2">
<div id=@answer.Id class="voting flex flex-col items-center">
Expand All @@ -128,12 +131,16 @@
<div class="hidden md:block text-center whitespace-nowrap text-xs xl:text-sm font-semibold">@userName</div>
</div>
</div>
<div class="xl:flex-grow prose">
<div id=@($"preview-{answer.Id}") class="preview xl:flex-grow prose">
@BlazorHtml.Raw(Markdown.GenerateHtml(answer.Choices.FirstOrDefault()?.Message.Content))
</div>
<div id=@($"edit-{answer.Id}") class="edit w-full pl-2 hidden"></div>
</div>
<div class="mt-4 flex justify-end text-gray-700 dark:text-gray-200 text-sm">
answered <time class="ml-2" datetime="@Markdown.GetDateTimestamp(DateTimeOffset.FromUnixTimeSeconds(answer.Created).DateTime)">@Markdown.GetDateLabel(DateTimeOffset.FromUnixTimeSeconds(answer.Created).DateTime)</time>
<div class="mt-6 flex justify-between">
<div class="pl-10 md:pl-20 xl:pl-32"><span class="edit-link cursor-pointer select-none text-indigo-700 dark:text-indigo-300 hover:text-indigo-500"></span></div>
<div class="text-gray-700 dark:text-gray-200 text-sm">
answered <time class="ml-1" datetime="@Markdown.GetDateTimestamp(DateTimeOffset.FromUnixTimeSeconds(answer.Created).DateTime)">@Markdown.GetDateLabel(DateTimeOffset.FromUnixTimeSeconds(answer.Created).DateTime)</time>
</div>
</div>
</div>
@if (Question.GetAnswerComments(answer.Id).Count > 0)
Expand Down
2 changes: 1 addition & 1 deletion MyApp/Configure.Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public async Task Any(RenderComponent request)
foreach (var remoteFile in remoteFiles.Files)
{
var localFile = localFiles.Files.FirstOrDefault(x => x.Name == remoteFile.Name);
if (localFile == null || localFile.LastModified < remoteFile.LastModified)
if (localFile == null || localFile.Length != remoteFile.Length)
{
log.LogInformation("Saving local file for {State} {Path}", localFile == null ? "new" : "modified", remoteFile.VirtualPath);
var remoteContents = await remoteFile.ReadAllTextAsync();
Expand Down
2 changes: 1 addition & 1 deletion MyApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
services.AddIdentityCore<ApplicationUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._";
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.";
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
Expand Down
Loading

0 comments on commit 0bf742d

Please sign in to comment.