238 lines
7.9 KiB
Plaintext

package component
import (
"bytes"
"context"
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
"forge.cadoles.com/wpetit/clearcase/internal/http/form"
common "forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
"github.com/pkg/errors"
"github.com/yuin/goldmark"
"log/slog"
"strings"
)
type IssuePageVModel struct {
IssueURL string
SummaryForm *form.Form
IssueForm *form.Form
IssueTips string
Projects []*model.Project
SelectedProjectID string
}
const summaryPlaceholder = `
Décrivez rapidement le sujet du problème rencontré ou de l'évolution souhaitée, ClearCase utilisera le modèle de demande présent dans le dépôt (ou un modèle par défaut) afin de générer une version mise en forme et complétée.
Afin de fournir plus d'information de contexte au LLM, vous pouvez faire référence à d'autres tickets du dépôt via un ou plusieurs '#<issue_id>' et/ou des chemins vers des fichiers présents dans celui ci.
`
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.
`
func NewIssueSummaryForm() *form.Form {
return form.New(
form.NewField(
"project",
form.Attrs{},
form.NonEmpty("Ce champs ne doit pas être vide."),
),
form.NewField(
"summary",
form.Attrs{
"type": "textarea",
"rows": "20",
"placeholder": strings.TrimSpace(summaryPlaceholder),
},
form.NonEmpty("Ce champ ne doit pas être vide."),
),
)
}
func NewIssueForm() *form.Form {
return form.New(
form.NewField(
"title",
form.Attrs{
"type": "text",
"placeholder": "Écrivez le résumé de votre demande et cliquez sur 'Générer' pour remplir automatiquement ces champs.",
},
form.NonEmpty("Ce champ ne doit pas être vide."),
),
form.NewField(
"body",
form.Attrs{
"type": "textarea",
"rows": "20",
"placeholder": strings.TrimSpace(bodyPlaceholder),
},
form.NonEmpty("Ce champ ne doit pas être vide."),
),
)
}
templ IssuePage(vmodel IssuePageVModel) {
@common.Page(common.WithTitle("Nouvelle demande")) {
<div class="container is-fluid">
<section class="section">
<div class="level is-mobile">
<div class="level-left">
<h1 class="title is-size-1"><span class="has-text-info-light">Clear</span><span>Case</span></h1>
</div>
<div class="level-right">
<div class="buttons is-right level-item">
<a class="button is-medium" href={ common.BaseURL(ctx, common.WithPath("/auth/logout")) }><span class="icon"><i class="fa fa-sign-out-alt"></i></span></a>
</div>
</div>
</div>
if vmodel.IssueURL != "" {
<article class="message is-primary">
<div class="message-header">
<p>Demande créée !</p>
<button class="delete" aria-label="delete" hx-on:click="onCloseMessage(this)"></button>
</div>
<div class="message-body">
Votre demande a été créée et est disponible à l'adresse suivante:
<a href={ templ.SafeURL(vmodel.IssueURL) } target="_blank"><code>{ vmodel.IssueURL }</code></a>.
</div>
</article>
<script type="text/javascript">
function clearSummary(projectId) {
sessionStorage.removeItem(`summary-${projectId}`)
}
function openIssue(issueUrl) {
window.open(issueUrl, "_blank");
}
</script>
@templ.JSFuncCall("clearSummary", vmodel.SelectedProjectID)
@templ.JSFuncCall("openIssue", vmodel.IssueURL)
}
<div class="columns">
<div class="column is-4">
<form id="summary-form" action={ common.CurrentURL(ctx) } method="put" hx-disabled-elt="textarea, input, select, button" hx-on:htmx:before-send="savePreferredProject()" hx-indicator="#generation-progress">
<h2 class="title is-size-3">Résumé de la demande</h2>
@common.FormSelect(
vmodel.SummaryForm, "issue-project", "project", "Projet",
common.WithOptions(projectsToOptions(vmodel.Projects)...),
common.WithAttrs(
"hx-get", string(common.CurrentURL(ctx, common.WithoutValues("project", "*"))),
"hx-target", "body",
"hx-push-url", "true",
),
)
@common.FormTextarea(
vmodel.SummaryForm, "issue-summary", "summary", "Résumé",
common.WithTextareaAttrs(
"hx-on:change", "onSummaryChange(event)",
),
)
<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={ common.CurrentURL(ctx) } method="post" hx-disabled-elt="textarea, input, select, button" hx-indicator="#generation-progress">
@common.FormField(vmodel.IssueForm, "issue-title", "title", "Titre")
@common.FormTextarea(vmodel.IssueForm, "issue-body", "body", "Corps")
<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 vmodel.IssueTips != "" {
{{ html := markdownToHTML(ctx, vmodel.IssueTips) }}
if html != "" {
<article class="message is-info mt-5">
<div class="message-header">
<p><span class="icon"><i class="fa fa-lightbulb"></i></span>Suggestions</p>
<button class="delete" aria-label="delete" hx-on:click="onCloseMessage(this)"></button>
</div>
<div class="message-body">
<div class="content">
@templ.Raw(html)
</div>
</div>
</article>
}
}
</section>
</div>
<script type="text/javascript">
function onCloseMessage(closeElement) {
closeElement.closest('.message').style.display = 'none';
}
function onSummaryChange(evt) {
const summary = evt.currentTarget.value;
const projectId = document.getElementById("issue-project").value;
sessionStorage.setItem(`summary-${projectId}`, summary);
}
function savePreferredProject() {
const projectId = document.getElementById("issue-project").value;
localStorage.setItem(`preferred-project`, projectId);
}
function restorePreferredProject() {
const preferredProject = localStorage.getItem(`preferred-project`);
if (!preferredProject) return;
const projectElement = document.getElementById("issue-project");
if (!projectElement) return;
if (preferredProject === projectElement.value) return;
projectElement.value = preferredProject;
projectElement.dispatchEvent(new Event('change'));
}
function restoreLastSummary() {
const summaryTextarea = document.getElementById("issue-summary");
if (!summaryTextarea) return;
const summary = summaryTextarea.value;
if (summary !== "") return;
const projectId = document.getElementById("issue-project").value;
if (!projectId) return;
const savedSummary = sessionStorage.getItem(`summary-${projectId}`);
if (!savedSummary) return;
summaryTextarea.value = savedSummary;
}
htmx.onLoad(function(){
restoreLastSummary();
restorePreferredProject();
})
</script>
}
}
func projectsToOptions(projects []*model.Project) []string {
options := make([]string, 0, len(projects)*2)
for _, p := range projects {
options = append(options, p.Name, p.ID)
}
return options
}
func markdownToHTML(ctx context.Context, text string) string {
var buff bytes.Buffer
if err := goldmark.Convert([]byte(text), &buff); err != nil {
slog.ErrorContext(ctx, "could not convert markdown to html", slog.Any("error", errors.WithStack(err)))
return ""
}
return buff.String()
}