feat: overwrite project issue template
This commit is contained in:
parent
08cdb44490
commit
406aa46a5a
2
go.mod
2
go.mod
@ -7,7 +7,7 @@ toolchain go1.23.6
|
||||
require (
|
||||
code.gitea.io/sdk/gitea v0.20.0
|
||||
github.com/a-h/templ v0.3.833
|
||||
github.com/bornholm/genai v0.0.0-20250222092500-1076426da67c
|
||||
github.com/bornholm/genai v0.0.0-20250227201654-4c93b20ee628
|
||||
github.com/caarlos0/env/v11 v11.2.2
|
||||
github.com/gabriel-vasile/mimetype v1.4.7
|
||||
github.com/google/go-github/v69 v69.2.0
|
||||
|
2
go.sum
2
go.sum
@ -14,6 +14,8 @@ github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
|
||||
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
|
||||
github.com/bornholm/genai v0.0.0-20250222092500-1076426da67c h1:bI0ebsgO1/7Jx6+ZQdDF/I6tTZxyB5hODYz7x/XxwK8=
|
||||
github.com/bornholm/genai v0.0.0-20250222092500-1076426da67c/go.mod h1:MnuvwSsBEWv/joeK/WgUyfZfOLcLTpd81NJdWoRpRfI=
|
||||
github.com/bornholm/genai v0.0.0-20250227201654-4c93b20ee628 h1:YsrF9+NUdwYPLfpJUUfD0h/yH0jvpnaMxtM/sPsFsPg=
|
||||
github.com/bornholm/genai v0.0.0-20250227201654-4c93b20ee628/go.mod h1:kgZb50LiE3cLjyGdUzNwDtpxL5QRllZIsWT2Ub24fIM=
|
||||
github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg=
|
||||
github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
|
@ -85,8 +85,13 @@ func (m *IssueManager) GetUserProjects(ctx context.Context, user *model.User) ([
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func (m *IssueManager) GenerateIssue(ctx context.Context, user *model.User, projectID string, issueSummary string) (string, string, string, error) {
|
||||
systemPrompt, err := m.getIssueSystemPrompt(ctx, user, projectID)
|
||||
type GeneratIssueOptions struct {
|
||||
IssueSummary string
|
||||
IssueTemplate string
|
||||
}
|
||||
|
||||
func (m *IssueManager) GenerateIssue(ctx context.Context, user *model.User, projectID string, issueSummary string, overwrittenIssueTemplate string) (string, string, string, error) {
|
||||
systemPrompt, err := m.getIssueSystemPrompt(ctx, user, projectID, overwrittenIssueTemplate)
|
||||
if err != nil {
|
||||
return "", "", "", errors.WithStack(err)
|
||||
}
|
||||
@ -135,15 +140,19 @@ func (m *IssueManager) GenerateIssue(ctx context.Context, user *model.User, proj
|
||||
return title, body, tips, nil
|
||||
}
|
||||
|
||||
func (m *IssueManager) getIssueSystemPrompt(ctx context.Context, user *model.User, projectID string) (string, error) {
|
||||
forge, err := m.getUserForge(ctx, user)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
func (m *IssueManager) getIssueSystemPrompt(ctx context.Context, user *model.User, projectID string, issueTemplate string) (string, error) {
|
||||
if issueTemplate == "" {
|
||||
forge, err := m.getUserForge(ctx, user)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
issueTemplate, err := forge.GetIssueTemplate(ctx, projectID)
|
||||
if err != nil && !errors.Is(err, port.ErrFileNotFound) {
|
||||
return "", errors.WithStack(err)
|
||||
repoIssueTemplate, err := forge.GetIssueTemplate(ctx, projectID)
|
||||
if err != nil && !errors.Is(err, port.ErrFileNotFound) {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
issueTemplate = repoIssueTemplate
|
||||
}
|
||||
|
||||
if issueTemplate == "" {
|
||||
|
@ -31,6 +31,10 @@ const bodyPlaceholder = `
|
||||
Une fois votre demande générée, vous pourrez l'éditer puis la créer directement en cliquant sur le bouton 'Créer' ci-dessous.
|
||||
`
|
||||
|
||||
const issueTemplatePlaceholder = `
|
||||
Vous pouvez surcharger le modèle de demande fourni par le projet en remplissant ce champ.
|
||||
`
|
||||
|
||||
func NewIssueSummaryForm() *form.Form {
|
||||
return form.New(
|
||||
form.NewField(
|
||||
@ -47,6 +51,14 @@ func NewIssueSummaryForm() *form.Form {
|
||||
},
|
||||
form.NonEmpty("Ce champ ne doit pas être vide."),
|
||||
),
|
||||
form.NewField(
|
||||
"template",
|
||||
form.Attrs{
|
||||
"type": "textarea",
|
||||
"rows": "20",
|
||||
"placeholder": strings.TrimSpace(issueTemplatePlaceholder),
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ -127,6 +139,15 @@ templ IssuePage(vmodel IssuePageVModel) {
|
||||
"hx-on:change", "onSummaryChange(event)",
|
||||
),
|
||||
)
|
||||
<details class="my-3">
|
||||
<summary class="is-clickable">Paramètres avancés</summary>
|
||||
@common.FormTextarea(
|
||||
vmodel.SummaryForm, "issue-template", "template", "Surcharger le modèle de demande",
|
||||
common.WithTextareaAttrs(
|
||||
"hx-on:change", "onIssueTemplateChange(event)",
|
||||
),
|
||||
)
|
||||
</details>
|
||||
<div class="buttons is-right">
|
||||
<button type="submit" class="button is-info is-large">
|
||||
<span class="icon">
|
||||
@ -184,6 +205,12 @@ templ IssuePage(vmodel IssuePageVModel) {
|
||||
sessionStorage.setItem(`summary-${projectId}`, summary);
|
||||
}
|
||||
|
||||
function onIssueTemplateChange(evt) {
|
||||
const issueTemplate = evt.currentTarget.value;
|
||||
const projectId = document.getElementById("issue-project").value;
|
||||
localStorage.setItem(`issue-template-${projectId}`, issueTemplate);
|
||||
}
|
||||
|
||||
function savePreferredProject() {
|
||||
const projectId = document.getElementById("issue-project").value;
|
||||
localStorage.setItem(`preferred-project`, projectId);
|
||||
@ -211,9 +238,22 @@ templ IssuePage(vmodel IssuePageVModel) {
|
||||
summaryTextarea.value = savedSummary;
|
||||
}
|
||||
|
||||
function restoreIssueTemplate() {
|
||||
const issueTemplateTextarea = document.getElementById("issue-template");
|
||||
if (!issueTemplateTextarea) return;
|
||||
const issueTemplate = issueTemplateTextarea.value;
|
||||
if (issueTemplate !== "") return;
|
||||
const projectId = document.getElementById("issue-project").value;
|
||||
if (!projectId) return;
|
||||
const savedIssueTemplate = localStorage.getItem(`issue-template-${projectId}`);
|
||||
if (!savedIssueTemplate) return;
|
||||
issueTemplateTextarea.value = savedIssueTemplate;
|
||||
}
|
||||
|
||||
htmx.onLoad(function(){
|
||||
restoreLastSummary();
|
||||
restorePreferredProject();
|
||||
restoreIssueTemplate();
|
||||
})
|
||||
</script>
|
||||
}
|
||||
|
@ -39,6 +39,10 @@ const bodyPlaceholder = `
|
||||
Une fois votre demande générée, vous pourrez l'éditer puis la créer directement en cliquant sur le bouton 'Créer' ci-dessous.
|
||||
`
|
||||
|
||||
const issueTemplatePlaceholder = `
|
||||
Vous pouvez surcharger le modèle de demande fourni par le projet en remplissant ce champ.
|
||||
`
|
||||
|
||||
func NewIssueSummaryForm() *form.Form {
|
||||
return form.New(
|
||||
form.NewField(
|
||||
@ -55,6 +59,14 @@ func NewIssueSummaryForm() *form.Form {
|
||||
},
|
||||
form.NonEmpty("Ce champ ne doit pas être vide."),
|
||||
),
|
||||
form.NewField(
|
||||
"template",
|
||||
form.Attrs{
|
||||
"type": "textarea",
|
||||
"rows": "20",
|
||||
"placeholder": strings.TrimSpace(issueTemplatePlaceholder),
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ -143,7 +155,7 @@ func IssuePage(vmodel IssuePageVModel) templ.Component {
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(vmodel.IssueURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/issue/component/issue_page.templ`, Line: 97, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/issue/component/issue_page.templ`, Line: 109, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -200,7 +212,20 @@ func IssuePage(vmodel IssuePageVModel) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"buttons is-right\"><button type=\"submit\" class=\"button is-info is-large\"><span class=\"icon\"><i class=\"fa fa-robot\"></i></span> <span>Générer</span></button></div></form></div><div class=\"column\"><h2 class=\"title is-size-3\">Votre demande</h2><form action=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<details class=\"my-3\"><summary class=\"is-clickable\">Paramètres avancés</summary>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = common.FormTextarea(
|
||||
vmodel.SummaryForm, "issue-template", "template", "Surcharger le modèle de demande",
|
||||
common.WithTextareaAttrs(
|
||||
"hx-on:change", "onIssueTemplateChange(event)",
|
||||
),
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</details><div class=\"buttons is-right\"><button type=\"submit\" class=\"button is-info is-large\"><span class=\"icon\"><i class=\"fa fa-robot\"></i></span> <span>Générer</span></button></div></form></div><div class=\"column\"><h2 class=\"title is-size-3\">Votre demande</h2><form action=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -209,7 +234,7 @@ func IssuePage(vmodel IssuePageVModel) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" method=\"post\" hx-disabled-elt=\"textarea, input, select, button\" hx-indicator=\"#generation-progress\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" method=\"post\" hx-disabled-elt=\"textarea, input, select, button\" hx-indicator=\"#generation-progress\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -221,14 +246,14 @@ func IssuePage(vmodel IssuePageVModel) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"buttons is-right\"><button type=\"submit\" class=\"button is-info is-large\"><span class=\"icon\"><i class=\"fa fa-rocket\"></i></span> <span>Créer</span></button></div></form></div></div><progress id=\"generation-progress\" class=\"htmx-indicator progress\"></progress> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"buttons is-right\"><button type=\"submit\" class=\"button is-info is-large\"><span class=\"icon\"><i class=\"fa fa-rocket\"></i></span> <span>Créer</span></button></div></form></div></div><progress id=\"generation-progress\" class=\"htmx-indicator progress\"></progress> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if vmodel.IssueTips != "" {
|
||||
html := markdownToHTML(ctx, vmodel.IssueTips)
|
||||
if html != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<article class=\"message is-info mt-5\"><div class=\"message-header\"><p><span class=\"icon\"><i class=\"fa fa-lightbulb\"></i></span>Questionnements</p><button class=\"delete\" aria-label=\"delete\" hx-on:click=\"onCloseMessage(this)\"></button></div><div class=\"message-body\"><div class=\"content\"><p>Utilisez ces quelques questions pour réfléchir aux éléments d'informations nécessaire à la bonne rédaction de votre demande:</p>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<article class=\"message is-info mt-5\"><div class=\"message-header\"><p><span class=\"icon\"><i class=\"fa fa-lightbulb\"></i></span>Questionnements</p><button class=\"delete\" aria-label=\"delete\" hx-on:click=\"onCloseMessage(this)\"></button></div><div class=\"message-body\"><div class=\"content\"><p>Utilisez ces quelques questions pour réfléchir aux éléments d'informations nécessaire à la bonne rédaction de votre demande:</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -236,13 +261,13 @@ func IssuePage(vmodel IssuePageVModel) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></div></article>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></div></article>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</section></div><script type=\"text/javascript\">\n\t\tfunction onCloseMessage(closeElement) {\n\t\t\tcloseElement.closest('.message').style.display = 'none';\n\t\t}\n\n\t\tfunction onSummaryChange(evt) {\n\t\t\tconst summary = evt.currentTarget.value;\n\t\t\tconst projectId = document.getElementById(\"issue-project\").value;\n\t\t\tsessionStorage.setItem(`summary-${projectId}`, summary);\n\t\t}\n\n\t\tfunction savePreferredProject() {\n\t\t\tconst projectId = document.getElementById(\"issue-project\").value;\n\t\t\tlocalStorage.setItem(`preferred-project`, projectId);\n\t\t}\n\n\t\tfunction restorePreferredProject() {\n\t\t\tconst preferredProject = localStorage.getItem(`preferred-project`);\n\t\t\tif (!preferredProject) return;\n\t\t\tconst projectElement = document.getElementById(\"issue-project\");\n\t\t\tif (!projectElement) return;\n\t\t\tif (preferredProject === projectElement.value) return;\n\t\t\tprojectElement.value = preferredProject;\n\t\t\tprojectElement.dispatchEvent(new Event('change'));\n\t\t}\n\n\t\tfunction restoreLastSummary() {\n\t\t\tconst summaryTextarea = document.getElementById(\"issue-summary\");\n\t\t\tif (!summaryTextarea) return;\n const summary = summaryTextarea.value;\n\t\t\tif (summary !== \"\") return;\n\t\t\tconst projectId = document.getElementById(\"issue-project\").value;\n\t\t\tif (!projectId) return;\n\t\t\tconst savedSummary = sessionStorage.getItem(`summary-${projectId}`);\n\t\t\tif (!savedSummary) return;\n\t\t\tsummaryTextarea.value = savedSummary;\n\t\t}\n\n\t\thtmx.onLoad(function(){\n\t\t\trestoreLastSummary();\n\t\t\trestorePreferredProject();\n })\n\t\t</script>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</section></div><script type=\"text/javascript\">\n\t\tfunction onCloseMessage(closeElement) {\n\t\t\tcloseElement.closest('.message').style.display = 'none';\n\t\t}\n\n\t\tfunction onSummaryChange(evt) {\n\t\t\tconst summary = evt.currentTarget.value;\n\t\t\tconst projectId = document.getElementById(\"issue-project\").value;\n\t\t\tsessionStorage.setItem(`summary-${projectId}`, summary);\n\t\t}\n\n\t\tfunction onIssueTemplateChange(evt) {\n\t\t\tconst issueTemplate = evt.currentTarget.value;\n\t\t\tconst projectId = document.getElementById(\"issue-project\").value;\n\t\t\tlocalStorage.setItem(`issue-template-${projectId}`, issueTemplate);\n\t\t}\n\n\t\tfunction savePreferredProject() {\n\t\t\tconst projectId = document.getElementById(\"issue-project\").value;\n\t\t\tlocalStorage.setItem(`preferred-project`, projectId);\n\t\t}\n\n\t\tfunction restorePreferredProject() {\n\t\t\tconst preferredProject = localStorage.getItem(`preferred-project`);\n\t\t\tif (!preferredProject) return;\n\t\t\tconst projectElement = document.getElementById(\"issue-project\");\n\t\t\tif (!projectElement) return;\n\t\t\tif (preferredProject === projectElement.value) return;\n\t\t\tprojectElement.value = preferredProject;\n\t\t\tprojectElement.dispatchEvent(new Event('change'));\n\t\t}\n\n\t\tfunction restoreLastSummary() {\n\t\t\tconst summaryTextarea = document.getElementById(\"issue-summary\");\n\t\t\tif (!summaryTextarea) return;\n const summary = summaryTextarea.value;\n\t\t\tif (summary !== \"\") return;\n\t\t\tconst projectId = document.getElementById(\"issue-project\").value;\n\t\t\tif (!projectId) return;\n\t\t\tconst savedSummary = sessionStorage.getItem(`summary-${projectId}`);\n\t\t\tif (!savedSummary) return;\n\t\t\tsummaryTextarea.value = savedSummary;\n\t\t}\n\n\t\tfunction restoreIssueTemplate() {\n\t\t\tconst issueTemplateTextarea = document.getElementById(\"issue-template\");\n\t\t\tif (!issueTemplateTextarea) return;\n const issueTemplate = issueTemplateTextarea.value;\n\t\t\tif (issueTemplate !== \"\") return;\n\t\t\tconst projectId = document.getElementById(\"issue-project\").value;\n\t\t\tif (!projectId) return;\n\t\t\tconst savedIssueTemplate = localStorage.getItem(`issue-template-${projectId}`);\n\t\t\tif (!savedIssueTemplate) return;\n\t\t\tissueTemplateTextarea.value = savedIssueTemplate;\n\t\t}\n\n\t\thtmx.onLoad(function(){\n\t\t\trestoreLastSummary();\n\t\t\trestorePreferredProject();\n\t\t\trestoreIssueTemplate();\n })\n\t\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package issue
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/core/service"
|
||||
httpCtx "forge.cadoles.com/wpetit/clearcase/internal/http/context"
|
||||
@ -99,13 +100,21 @@ func (h *Handler) handleIssueSummaryForm(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
summary, err := form.FormFieldAttr[string](issueSummaryForm, "summary", "value")
|
||||
issueSummary, err := form.FormFieldAttr[string](issueSummaryForm, "summary", "value")
|
||||
if err != nil {
|
||||
h.handleError(w, r, errors.WithStack(err))
|
||||
return
|
||||
}
|
||||
|
||||
issueTitle, issueBody, issueTips, err := h.issueManager.GenerateIssue(ctx, httpCtx.User(ctx), projectID, summary)
|
||||
issueTemplate, err := form.FormFieldAttr[string](issueSummaryForm, "template", "value")
|
||||
if err != nil {
|
||||
h.handleError(w, r, errors.WithStack(err))
|
||||
return
|
||||
}
|
||||
|
||||
issueTemplate = strings.TrimSpace(issueTemplate)
|
||||
|
||||
issueTitle, issueBody, issueTips, err := h.issueManager.GenerateIssue(ctx, httpCtx.User(ctx), projectID, issueSummary, issueTemplate)
|
||||
if err != nil {
|
||||
h.handleError(w, r, errors.WithStack(err))
|
||||
return
|
||||
|
@ -13,13 +13,13 @@ import (
|
||||
)
|
||||
|
||||
func NewIssueManagerFromConfig(ctx context.Context, conf *config.Config) (*service.IssueManager, error) {
|
||||
llmCtx := provider.FromMap(ctx, "", map[string]string{
|
||||
string(provider.ContextKeyAPIBaseURL): conf.LLM.Provider.BaseURL,
|
||||
string(provider.ContextKeyAPIKey): conf.LLM.Provider.Key,
|
||||
string(provider.ContextKeyModel): conf.LLM.Provider.Model,
|
||||
})
|
||||
|
||||
client, err := provider.Create(llmCtx, provider.Name(conf.LLM.Provider.Name))
|
||||
client, err := provider.Create(ctx,
|
||||
provider.WithConfig(&provider.Config{
|
||||
Provider: provider.Name(conf.LLM.Provider.Name),
|
||||
BaseURL: conf.LLM.Provider.BaseURL,
|
||||
Key: conf.LLM.Provider.Key,
|
||||
Model: conf.LLM.Provider.Model,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not create llm client '%s'", conf.LLM.Provider.Name)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user