Skip to content

Commit

Permalink
Implement markdown comments + Ask Model
Browse files Browse the repository at this point in the history
  • Loading branch information
mythz committed May 4, 2024
1 parent 0c2e5e4 commit f1c8333
Show file tree
Hide file tree
Showing 13 changed files with 3,880 additions and 254 deletions.
53 changes: 46 additions & 7 deletions MyApp.ServiceInterface/Data/AppConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,18 @@ public class AppConfig
public HashSet<string> AllTags { get; set; } = [];
public List<ApplicationUser> ModelUsers { get; set; } = [];

public static string[] DeprecatedModels = ["deepseek-coder","gemma-2b","qwen-4b","deepseek-coder-33b"];

public static (string Model, int Questions)[] GetActiveModelsForQuestions(int level) =>
ModelsForQuestions.Where(x => x.Questions == level && !DeprecatedModels.Contains(x.Model)).ToArray();

public static (string Model, int Questions)[] ModelsForQuestions =
[
("deepseek-coder", 0),
("gemma-2b", 0),
("qwen-4b", 0),
("deepseek-coder-33b", 100),

("phi", 0),
("codellama", 0),
("mistral", 0),
Expand All @@ -49,12 +59,13 @@ public static (string Model, int Questions)[] ModelsForQuestions =
("mixtral", 5),
("gpt3.5-turbo", 10),
("claude3-haiku", 25),
("command-r", 50),
("wizardlm", 100),
("claude3-sonnet", 175),
("command-r-plus", 150),
("gpt4-turbo", 250),
("claude3-opus", 400),
("llama3-70b", 50),
("command-r", 100),
("wizardlm", 175),
("claude3-sonnet", 250),
("command-r-plus", 350),
("gpt4-turbo", 450),
("claude3-opus", 600),
];

public static int[] QuestionLevels = ModelsForQuestions.Select(x => x.Questions).Distinct().OrderBy(x => x).ToArray();
Expand Down Expand Up @@ -106,6 +117,26 @@ public int GetReputationValue(string? userName) =>
? 1
: reputation;

public bool CanUseModel(string userName, string model)
{
if (Stats.IsAdminOrModerator(userName))
return true;
var questionsCount = GetQuestionCount(userName);

var modelLevel = GetModelLevel(model);
return modelLevel != -1 && questionsCount >= modelLevel;
}

public int GetModelLevel(string model)
{
foreach (var entry in ModelsForQuestions)
{
if (entry.Model == model)
return entry.Questions;
}
return -1;
}

public int GetQuestionCount(string? userName) => userName switch
{
"stackoverflow" or "reddit" or "discourse" => 5,
Expand Down Expand Up @@ -207,13 +238,21 @@ public List<string> GetAnswerModelUsersFor(string? userName)
{
var questionsCount = GetQuestionCount(userName);

var models = ModelsForQuestions.Where(x => x.Questions <= questionsCount)
var models = GetActiveModelsForQuestions(questionsCount)
.Select(x => x.Model)
.ToList();

// Remove lower quality models
if (models.Contains("gemma"))
models.RemoveAll(x => x == "gemma:2b");
if (models.Contains("mixtral"))
models.RemoveAll(x => x == "mistral");
if (models.Contains("deepseek-coder:33b"))
models.RemoveAll(x => x == "deepseek-coder:6.7b");
if (models.Contains("gpt-4-turbo"))
models.RemoveAll(x => x == "gpt3.5-turbo");
if (models.Contains("command-r-plus"))
models.RemoveAll(x => x == "command-r");
if (models.Contains("claude-3-opus"))
models.RemoveAll(x => x is "claude-3-haiku" or "claude-3-sonnet");
if (models.Contains("claude-3-sonnet"))
Expand Down
33 changes: 23 additions & 10 deletions MyApp.ServiceInterface/QuestionServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,15 +350,35 @@ public async Task<object> Any(CreateComment request)
if (question.LockedDate != null)
throw HttpError.Conflict($"{question.GetPostType()} is locked");

var createdBy = GetUserName();

// If the comment is for a model answer, have the model respond to the comment
var answerCreator = request.Id.Contains('-')
? request.Id.RightPart('-')
: null;
var modelCreator = answerCreator != null
? appConfig.GetModelUser(answerCreator)
: null;

if (modelCreator?.UserName != null)
{
var canUseModel = appConfig.CanUseModel(createdBy, modelCreator.UserName);
if (!canUseModel)
{
var userCount = appConfig.GetQuestionCount(createdBy);
log.LogWarning("User {UserName} ({UserCount}) attempted to use model {ModelUserName} ({ModelCount})",
createdBy, userCount, modelCreator.UserName, appConfig.GetModelLevel(modelCreator.UserName));
throw HttpError.Forbidden("You have not met the requirements to access this model");
}
}

var postId = question.Id;
var meta = await questions.GetMetaAsync(postId);

meta.Comments ??= new();
var comments = meta.Comments.GetOrAdd(request.Id, key => new());
var body = request.Body.GenerateComment();

var createdBy = GetUserName();

var newComment = new Comment
{
Body = body,
Expand All @@ -378,14 +398,7 @@ public async Task<object> Any(CreateComment request)

await questions.SaveMetaAsync(postId, meta);

// If the comment is for a model answer, have the model respond to the comment
var answerCreator = request.Id.Contains('-')
? request.Id.RightPart('-')
: null;
var modelCreator = answerCreator != null
? appConfig.GetModelUser(answerCreator)
: null;
if (modelCreator != null)
if (modelCreator?.UserName != null)
{
var answer = await questions.GetAnswerAsPostAsync(request.Id);
if (answer != null)
Expand Down
1 change: 1 addition & 0 deletions MyApp.ServiceInterface/UserServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public async Task<object> Any(UserPostData request)
var to = new UserPostDataResponse
{
Watching = watchingPost,
QuestionsAsked = appConfig.GetQuestionCount(userName),
UpVoteIds = allUserPostVotes.Where(x => x.Score > 0).Select(x => x.RefId).ToSet(),
DownVoteIds = allUserPostVotes.Where(x => x.Score < 0).Select(x => x.RefId).ToSet(),
};
Expand Down
3 changes: 2 additions & 1 deletion MyApp/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@
["app.mjs"] = ($"{baseUrl}/mjs/app.mjs", $"{baseUrl}/mjs/app.mjs"),
["dtos.mjs"] = ($"{baseUrl}/mjs/dtos.mjs", $"{baseUrl}/mjs/dtos.mjs"),
["vue"] = ($"{baseUrl}/lib/mjs/vue.mjs", $"{baseUrl}/lib/mjs/vue.min.mjs"),
["marked"] = ($"{baseUrl}/lib/mjs/marked.mjs", $"{baseUrl}/lib/mjs/marked.min.mjs"),
["markdown"] = ($"{baseUrl}/lib/mjs/markdown.mjs", $"{baseUrl}/lib/mjs/markdown.mjs"),
["@servicestack/client"] = ($"{baseUrl}/lib/mjs/servicestack-client.mjs", $"{baseUrl}/lib/mjs/servicestack-client.min.mjs"),
["@servicestack/vue"] = ($"{baseUrl}/lib/mjs/servicestack-vue.mjs", $"{baseUrl}/lib/mjs/servicestack-vue.min.mjs"),
})

<HeadOutlet />
</head>

Expand Down
2 changes: 1 addition & 1 deletion MyApp/Components/Pages/Questions/Ask.razor
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
}

<ul role="list" class=@CssUtils.ClassNames("border-2 py-2 px-4 rounded", qualified ? "border-green-600" : "border-gray-200")>
@foreach (var model in AppConfig.ModelsForQuestions.Where(x => x.Questions == questionLevel))
@foreach (var model in AppConfig.GetActiveModelsForQuestions(questionLevel))
{
var assistant = AppConfig.GetModelUser(model.Model)!;
<li class="mb-2">
Expand Down
4 changes: 3 additions & 1 deletion MyApp/Components/Shared/PostComments.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
@foreach (var comment in Comments)
{
<div id=@($"{RefId}-{comment.Created}") data-id=@RefId data-created=@comment.Created data-createdby=@comment.CreatedBy class="py-2 border-b border-gray-100 dark:border-gray-800 text-sm text-gray-600 dark:text-gray-300 prose prose-comment">
@BlazorHtml.Raw(Markdown.GenerateCommentHtml(comment.Body))
<span class="preview prose">
@BlazorHtml.Raw(Markdown.GenerateCommentHtml(comment.Body))
</span>
<span class="inline-block">
<span class="px-1" aria-hidden="true">&middot;</span>
<span class="text-indigo-700">@comment.CreatedBy</span>
Expand Down
18 changes: 18 additions & 0 deletions MyApp/wwwroot/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,10 @@ select{
margin-left: -0.25rem;
}

.-ml-12 {
margin-left: -3rem;
}

.-ml-20 {
margin-left: -5rem;
}
Expand Down Expand Up @@ -3451,6 +3455,16 @@ select{
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}

.\!invert {
--tw-invert: invert(100%) !important;
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow) !important;
}

.invert {
--tw-invert: invert(100%);
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}

.\!filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow) !important;
}
Expand Down Expand Up @@ -3545,6 +3559,10 @@ select{
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
}

.\[a-zA-Z\:_\] {
a-z-a--z: ;
}

.\[index\:string\] {
index: string;
}
Expand Down
104 changes: 104 additions & 0 deletions MyApp/wwwroot/lib/mjs/markdown.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { MarkedMin } from "./marked.min.mjs"

const marked = new MarkedMin(
markedHighlight({
langPrefix: 'hljs language-',
highlight(code, lang, info) {
const hljs = globalThis.hljs
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
return hljs.highlight(code, { language }).value
}
})
)

export async function renderMarkdown(body) {
const rawHtml = marked.parse(body, { async:false })
return rawHtml
}

export function markedHighlight(options) {
if (typeof options === 'function') {
options = {
highlight: options
}
}

if (!options || typeof options.highlight !== 'function') {
throw new Error('Must provide highlight function')
}

if (typeof options.langPrefix !== 'string') {
options.langPrefix = 'language-'
}

return {
async: !!options.async,
walkTokens(token) {
if (token.type !== 'code') {
return
}

const lang = getLang(token.lang)

if (options.async) {
return Promise.resolve(options.highlight(token.text, lang, token.lang || '')).then(updateToken(token))
}

const code = options.highlight(token.text, lang, token.lang || '')
if (code instanceof Promise) {
throw new Error('markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.')
}
updateToken(token)(code)
},
renderer: {
code(code, infoString, escaped) {
const lang = getLang(infoString)
const classAttr = lang
? ` class="${options.langPrefix}${escape(lang)}"`
: ' class="hljs"';
code = code.replace(/\n$/, '')
return `<pre><code${classAttr}>${escaped ? code : escape(code, true)}\n</code></pre>`
}
}
}
}

function getLang(lang) {
return (lang || '').match(/\S*/)[0]
}

function updateToken(token) {
return code => {
if (typeof code === 'string' && code !== token.text) {
token.escaped = true
token.text = code
}
}
}

// copied from marked helpers
const escapeTest = /[&<>"']/
const escapeReplace = new RegExp(escapeTest.source, 'g')
const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/
const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g')
const escapeReplacements = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}
const getEscapeReplacement = ch => escapeReplacements[ch]
function escape(html, encode) {
if (encode) {
if (escapeTest.test(html)) {
return html.replace(escapeReplace, getEscapeReplacement)
}
} else {
if (escapeTestNoEncode.test(html)) {
return html.replace(escapeReplaceNoEncode, getEscapeReplacement)
}
}

return html
}
8 changes: 8 additions & 0 deletions MyApp/wwwroot/lib/mjs/marked.min.mjs

Large diffs are not rendered by default.

Loading

0 comments on commit f1c8333

Please sign in to comment.