Compare commits
5 Commits
v1.0.0-bet
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8201a2f310 | ||
|
|
b2686ea602 | ||
|
|
bf8ea6103e | ||
|
|
88f4998601 | ||
|
|
5d3fa86627 |
@@ -3,7 +3,6 @@ package tasks
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
@@ -23,9 +22,7 @@ Your task:
|
||||
const desc = `There is a major logical error in this code, you need to fix it.
|
||||
Change the function logic so that it correctly adds two numbers together instead of subtracting them.`
|
||||
|
||||
var textFileDescription = `
|
||||
|
||||
# Instructions for the coding task
|
||||
var textFileDescription = `# Instructions for the coding task
|
||||
|
||||
` + desc + `
|
||||
############################################################`
|
||||
@@ -36,7 +33,7 @@ function Add(a, b int) int {
|
||||
}
|
||||
`
|
||||
|
||||
var textFileContent = codingDescription + textFileDescription + "\n" + code
|
||||
var textFileContent = textFileDescription + "\n" + code
|
||||
|
||||
type CodingTask struct {
|
||||
done bool
|
||||
@@ -107,41 +104,32 @@ func (t *CodingTask) Setup(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start file watcher goroutine
|
||||
t.startFileWatcher(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *CodingTask) startFileWatcher(ctx context.Context) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
lastMod := t.lastModified
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if t.app.SubmitChan == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
stat, err := os.Stat("./code.txt")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if stat.ModTime().After(t.lastModified) {
|
||||
t.lastModified = stat.ModTime()
|
||||
select {
|
||||
case t.app.SubmitChan <- models.ValidationTrigger{Source: models.ValidationTriggerAutoPoll}:
|
||||
default:
|
||||
if stat, err := os.Stat("./code.txt"); err == nil {
|
||||
if stat.ModTime().After(lastMod) {
|
||||
lastMod = stat.ModTime()
|
||||
if t.app.SubmitChan != nil {
|
||||
select {
|
||||
case t.app.SubmitChan <- models.ValidationTrigger{Source: models.ValidationTriggerAutoPoll}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback {
|
||||
@@ -160,19 +148,13 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback {
|
||||
return expect.ValidationFeedback
|
||||
}
|
||||
|
||||
fileContent, err := os.ReadFile("./code.txt")
|
||||
code, err := os.ReadFile("./code.txt")
|
||||
if err != nil {
|
||||
expect.Fail("Error reading code.txt file: " + err.Error())
|
||||
return expect.ValidationFeedback
|
||||
}
|
||||
|
||||
code, ok := strings.CutPrefix(string(fileContent), textFileDescription+"\n")
|
||||
if !ok {
|
||||
expect.Fail("The content of code.txt does not match the expected format.")
|
||||
return expect.ValidationFeedback
|
||||
}
|
||||
|
||||
expect.Contains(code, "a + b", "Function logic")
|
||||
expect.Contains(string(code), "a + b", "Function logic")
|
||||
if !expect.Valid() {
|
||||
return expect.ValidationFeedback
|
||||
}
|
||||
|
||||
@@ -104,44 +104,36 @@ func (t *GitTask) Setup(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize lastModified and start file watcher
|
||||
if stat, err := os.Stat("./task/README.md"); err == nil {
|
||||
t.lastModified = stat.ModTime()
|
||||
}
|
||||
t.startFileWatcher(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *GitTask) startFileWatcher(ctx context.Context) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
var lastMod time.Time
|
||||
|
||||
if stat, err := os.Stat("./task/.git/index"); err == nil {
|
||||
lastMod = stat.ModTime()
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if t.app.SubmitChan == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
stat, err := os.Stat("./task/README.md")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if stat.ModTime().After(t.lastModified) {
|
||||
t.lastModified = stat.ModTime()
|
||||
select {
|
||||
case t.app.SubmitChan <- models.ValidationTrigger{Source: models.ValidationTriggerAutoPoll}:
|
||||
default:
|
||||
if stat, err := os.Stat("./task/.git/index"); err == nil {
|
||||
if stat.ModTime().After(lastMod) {
|
||||
lastMod = stat.ModTime()
|
||||
if t.app.SubmitChan != nil {
|
||||
select {
|
||||
case t.app.SubmitChan <- models.ValidationTrigger{Source: models.ValidationTriggerAutoPoll}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *GitTask) Validate(ctx context.Context) ValidationFeedback {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package components
|
||||
|
||||
templ Status(msg string) {
|
||||
<div id="status" hx-get="/status" hx-trigger="load, click, task-status-check from:body" hx-target="#status" hx-swap="outerHTML">
|
||||
<div id="status" hx-get="/status" hx-trigger="load, click, check-status from:body" hx-target="#status" hx-swap="outerHTML">
|
||||
<button type="button" class="btn btn-outline btn-sm" hx-get="/status/modal" hx-target="#modal-container" hx-swap="innerHTML">{ msg }</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func Status(msg string) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"status\" hx-get=\"/status\" hx-trigger=\"load, click, task-status-check from:body\" hx-target=\"#status\" hx-swap=\"outerHTML\"><button type=\"button\" class=\"btn btn-outline btn-sm\" hx-get=\"/status/modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"status\" hx-get=\"/status\" hx-trigger=\"load, click, check-status from:body\" hx-target=\"#status\" hx-swap=\"outerHTML\"><button type=\"button\" class=\"btn btn-outline btn-sm\" hx-get=\"/status/modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -24,23 +24,32 @@ func SetSubmitChan(ch chan<- models.ValidationTrigger) {
|
||||
submitChan = ch
|
||||
}
|
||||
|
||||
func HandleTaskStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if submitChan != nil {
|
||||
select {
|
||||
case submitChan <- models.ValidationTrigger{Source: models.ValidationTriggerAutoPoll}:
|
||||
default:
|
||||
func SubmissionMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
next.ServeHTTP(w, r)
|
||||
if submitChan != nil {
|
||||
select {
|
||||
case submitChan <- models.ValidationTrigger{Source: models.ValidationTriggerAutoPoll}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func HandleTaskStatus(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
hx := HTMX(r)
|
||||
if hx.IsHxRequest() {
|
||||
if taskFeedback.Success {
|
||||
w.Header().Set("HX-Trigger", "task-status-success")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
hx.WriteString(`
|
||||
<div id="status" hx-get="/status" hx-trigger="click, check-status from:body" hx-target="#status" hx-swap="outerHTML">
|
||||
<button class="btn btn-success btn-sm" hx-get="/status/modal" hx-target="#modal-container" hx-swap="innerHTML" hx-trigger="intersect once">` + taskFeedback.Message + `</button>
|
||||
</div>`)
|
||||
} else {
|
||||
hx.WriteString(`
|
||||
<div id="status" type="button" hx-get="/status" hx-target="#status" hx-swap="outerHTML">
|
||||
<div id="status" hx-get="/status" hx-trigger="click, check-status from:body" hx-target="#status" hx-swap="outerHTML">
|
||||
<button class="btn btn-error btn-sm" hx-get="/status/modal" hx-target="#modal-container" hx-swap="innerHTML">` + taskFeedback.Message + `</button>
|
||||
</div>`)
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ templ DashboardContent(props DashboardProps) {
|
||||
</div>
|
||||
</template>
|
||||
<template x-for="issue in issues" :key="issue.id">
|
||||
<div x-show="issue.id === selectedId" class="issue-detail max-w-3xl mx-auto" x-data="{ editing: null, saving: false, newComment: '', async saveField(field, value) { const formData = new URLSearchParams(); formData.append(field, value || ''); await fetch('/issues/' + issue.id, { method: 'PATCH', body: formData, headers: { 'HX-Request': 'true' } }); this.editing = null; }, async addComment() { if (!this.newComment.trim()) return; const author = issue.created_by || 'Anonymous'; const response = await fetch('/issues/' + issue.id + '/comments', { method: 'POST', body: new URLSearchParams({text: this.newComment, author: author}) }); const newComment = await response.json(); if (!issue.comments) issue.comments = []; issue.comments.push(newComment); this.newComment = ''; } }">
|
||||
<div x-show="issue.id === selectedId" class="issue-detail max-w-3xl mx-auto" x-data="{ editing: null, saving: false, newComment: '', async saveField(field, value) { const formData = new URLSearchParams(); formData.append(field, value || ''); await fetch('/issues/' + issue.id, { method: 'PATCH', body: formData, headers: { 'HX-Request': 'true' } }); this.editing = null; setTimeout(() => { document.body.dispatchEvent(new CustomEvent('check-status')); }, 1000); }, async addComment() { if (!this.newComment.trim()) return; const author = issue.created_by || 'Anonymous'; const response = await fetch('/issues/' + issue.id + '/comments', { method: 'POST', body: new URLSearchParams({text: this.newComment, author: author}) }); const newComment = await response.json(); if (!issue.comments) issue.comments = []; issue.comments.push(newComment); this.newComment = ''; } }">
|
||||
<div class="card bg-base-100 shadow-md">
|
||||
<div class="card-body p-2">
|
||||
<div class="m-2">
|
||||
@@ -179,7 +179,7 @@ templ DashboardContent(props DashboardProps) {
|
||||
name="status"
|
||||
x-model="issue.status"
|
||||
class="select select-sm select-bordered"
|
||||
@change="fetch('/issues/' + issue.id, { method: 'PATCH', body: new URLSearchParams({status: issue.status}) }).then(() => editing = null)"
|
||||
@change="fetch('/issues/' + issue.id, { method: 'PATCH', body: new URLSearchParams({status: issue.status}), headers: { 'HX-Request': 'true' } }).then(() => { editing = null; setTimeout(() => { document.body.dispatchEvent(new CustomEvent('check-status')); }, 1000); })"
|
||||
x-init="$nextTick(() => $el.focus())"
|
||||
>
|
||||
<option value="open">Open</option>
|
||||
@@ -203,7 +203,7 @@ templ DashboardContent(props DashboardProps) {
|
||||
name="issue_type"
|
||||
x-model="issue.issue_type"
|
||||
class="select select-sm select-bordered"
|
||||
@change="fetch('/issues/' + issue.id, { method: 'PATCH', body: new URLSearchParams({issue_type: issue.issue_type}) }).then(() => editing = null)"
|
||||
@change="fetch('/issues/' + issue.id, { method: 'PATCH', body: new URLSearchParams({issue_type: issue.issue_type}), headers: { 'HX-Request': 'true' } }).then(() => { editing = null; setTimeout(() => { document.body.dispatchEvent(new CustomEvent('check-status')); }, 1000); })"
|
||||
x-init="$nextTick(() => $el.focus())"
|
||||
>
|
||||
<option value="task">Task</option>
|
||||
@@ -227,7 +227,7 @@ templ DashboardContent(props DashboardProps) {
|
||||
name="priority"
|
||||
x-model="issue.priority"
|
||||
class="select select-sm select-bordered"
|
||||
@change="fetch('/issues/' + issue.id, { method: 'PATCH', body: new URLSearchParams({priority: issue.priority}) }).then(() => editing = null)"
|
||||
@change="fetch('/issues/' + issue.id, { method: 'PATCH', body: new URLSearchParams({priority: issue.priority}), headers: { 'HX-Request': 'true' } }).then(() => { editing = null; setTimeout(() => { document.body.dispatchEvent(new CustomEvent('check-status')); }, 1000); })"
|
||||
x-init="$nextTick(() => $el.focus())"
|
||||
>
|
||||
<option value="0">Irrelevant (P0)</option>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -56,7 +56,11 @@ templ IssueDetailPage(issue *models.Issue, comments []*models.Comment) {
|
||||
headers: { 'HX-Request': 'true' }
|
||||
})
|
||||
this.editing = null
|
||||
// Check if server wants to redirect back to list
|
||||
// Check status after 1 second
|
||||
setTimeout(() => {
|
||||
document.body.dispatchEvent(new CustomEvent('check-status'))
|
||||
}, 1000)
|
||||
// Handle redirect if needed
|
||||
const redirectUrl = response.headers.get('HX-Redirect')
|
||||
if (redirectUrl) {
|
||||
window.location.href = redirectUrl
|
||||
|
||||
@@ -117,7 +117,11 @@ func IssueDetailPage(issue *models.Issue, comments []*models.Comment) templ.Comp
|
||||
headers: { 'HX-Request': 'true' }
|
||||
})
|
||||
this.editing = null
|
||||
// Check if server wants to redirect back to list
|
||||
// Check status after 1 second
|
||||
setTimeout(() => {
|
||||
document.body.dispatchEvent(new CustomEvent('check-status'))
|
||||
}, 1000)
|
||||
// Handle redirect if needed
|
||||
const redirectUrl = response.headers.get('HX-Redirect')
|
||||
if (redirectUrl) {
|
||||
window.location.href = redirectUrl
|
||||
@@ -157,7 +161,7 @@ func IssueDetailPage(issue *models.Issue, comments []*models.Comment) templ.Comp
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(xData)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 82, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 86, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -183,7 +187,7 @@ func IssueDetailPage(issue *models.Issue, comments []*models.Comment) templ.Comp
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(comment.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 282, Col: 61}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 286, Col: 61}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -196,7 +200,7 @@ func IssueDetailPage(issue *models.Issue, comments []*models.Comment) templ.Comp
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(comment.CreatedAt.Format("Jan 2, 2006 15:04"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 283, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 287, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -209,7 +213,7 @@ func IssueDetailPage(issue *models.Issue, comments []*models.Comment) templ.Comp
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(comment.Text)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 285, Col: 61}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 289, Col: 61}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -30,6 +30,7 @@ func (s *Server) RegisterRoutes(assets embed.FS) http.Handler {
|
||||
r.Use(handler.HTMXMiddleware)
|
||||
r.Use(handler.AppMiddleware(s.App))
|
||||
r.Use(actionLoggingMiddleware(s.App))
|
||||
r.Use(handler.SubmissionMiddleware)
|
||||
|
||||
s.handleAssets(r, assets)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user