Mise à jour du titre (imparfait)
This commit is contained in:
1 parent
345129c7bf
commit
509a3b14c8
14 files changed
+451
-18
No files matched your search
@@ -8,6 +8,7 @@ import (
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/file"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/llm"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/browser"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/searxng"
|
||||
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/weather"
|
||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
@@ -140,6 +141,7 @@ func GetToolService() *tool.Service {
|
||||
toolService = tool.NewService(db.ToolRepository(), db.UserRepository())
|
||||
toolService.RegisterToolImpl(&weather.WeatherTool{})
|
||||
toolService.RegisterToolImpl(&searxng.SearxngTool{})
|
||||
toolService.RegisterToolImpl(&browser.BrowserTool{})
|
||||
}
|
||||
return toolService
|
||||
}
|
||||
@@ -41,6 +41,15 @@ func (r *TursoChatRepository) ListChats(userID string, teamID string, page int)
|
||||
)
|
||||
}
|
||||
|
||||
func (r *TursoChatRepository) ListChatsWithUnfreshTitles(teamID string) ([]*domain.Chat, error) {
|
||||
return r.chatTable.Select(
|
||||
orm.WithWhere(
|
||||
"team_id=$1 and fresh_title=0",
|
||||
teamID,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *TursoChatRepository) GetChat(userID string, teamID string, id string) (*domain.Chat, error) {
|
||||
return r.chatTable.SelectOne(
|
||||
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
|
||||
|
||||
@@ -24,7 +24,7 @@ type TursoDB struct {
|
||||
func NewTursoDB(cfg *config.Config) (*TursoDB, error) {
|
||||
db, err := sql.Open("turso", cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error connecting to db %: %s", cfg.DatabaseURL, err)
|
||||
return nil, fmt.Errorf("error connecting to db %s: %s", cfg.DatabaseURL, err)
|
||||
}
|
||||
return &TursoDB{
|
||||
DB: db,
|
||||
|
||||
@@ -125,6 +125,7 @@ create table chats (
|
||||
user_id text,
|
||||
task_id text,
|
||||
title text,
|
||||
fresh_title numeric default 0,
|
||||
_date_created numeric not null default current_timestamp,
|
||||
_date_updated numeric,
|
||||
_version text not null
|
||||
|
||||
@@ -203,6 +203,40 @@ func (e *AnyLLMEngine) Stream(
|
||||
}
|
||||
}
|
||||
|
||||
func (e *AnyLLMEngine) Generate(
|
||||
ctx context.Context,
|
||||
provider *domain.Provider,
|
||||
modelID string,
|
||||
messages []*domain.Message,
|
||||
) (string, error) {
|
||||
|
||||
p, err := providerFactory(provider)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
anyllmMessages := make([]anyllm.Message, 0)
|
||||
for _, m := range messages {
|
||||
anyllmMessages = append(anyllmMessages, anyllm.Message{
|
||||
Role: m.Role,
|
||||
Content: m.Content,
|
||||
})
|
||||
}
|
||||
|
||||
response, err := p.Completion(ctx, anyllm.CompletionParams{
|
||||
Model: modelID,
|
||||
Messages: anyllmMessages,
|
||||
Stream: false,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return response.Choices[0].Message.Content.(string), nil
|
||||
|
||||
}
|
||||
|
||||
func toolConvert(domainTool *domain.ToolDefinition) providers.Tool {
|
||||
return providers.Tool{
|
||||
Type: domainTool.Type,
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/launcher"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
"github.com/go-rod/stealth"
|
||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
)
|
||||
|
||||
type BrowserConfig struct {
|
||||
// Headless est un pointeur pour distinguer "non renseigné" (défaut : true) de "explicitement false".
|
||||
Headless *bool `yaml:"headless"`
|
||||
TimeoutSeconds int `yaml:"timeoutSeconds"`
|
||||
}
|
||||
|
||||
// BrowserTool maintains a persistent browser session across LLM tool calls.
|
||||
type BrowserTool struct{}
|
||||
|
||||
var (
|
||||
browserInstance *rod.Browser
|
||||
currentPage *rod.Page
|
||||
browserMu sync.Mutex
|
||||
launchedHeadless bool
|
||||
)
|
||||
|
||||
func (t *BrowserTool) Name() string {
|
||||
return "browser"
|
||||
}
|
||||
|
||||
func (t *BrowserTool) Description() string {
|
||||
return "Control a web browser to navigate pages and interact with content. " +
|
||||
"Maintains session state across calls (cookies, current page). " +
|
||||
"Actions: 'navigate' (go to URL), 'click' (click element by CSS selector), " +
|
||||
"'fill' (type into an input field), 'content' (get current page text), " +
|
||||
"'links' (list all links on current page)."
|
||||
}
|
||||
|
||||
func (t *BrowserTool) Params() []domain.ToolParam {
|
||||
return []domain.ToolParam{
|
||||
{JsonName: "headless", Type: "bool", Mandatory: true, Description: "Headless"},
|
||||
{JsonName: "timeoutSeconds", Type: "int", Mandatory: true, Description: "Timeout en secondes"},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *BrowserTool) Definition(ctx context.Context) *domain.ToolDefinition {
|
||||
return &domain.ToolDefinition{
|
||||
Type: "function",
|
||||
Function: &domain.ToolFunction{
|
||||
Name: t.Name(),
|
||||
Description: t.Description(),
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"action": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []string{"navigate", "click", "fill", "content", "links"},
|
||||
"description": "The browser action to perform.",
|
||||
},
|
||||
"url": map[string]any{
|
||||
"type": "string",
|
||||
"description": "URL to navigate to. Required for action='navigate'.",
|
||||
},
|
||||
"selector": map[string]any{
|
||||
"type": "string",
|
||||
"description": "CSS selector of the element to interact with. Required for 'click' and 'fill'.",
|
||||
},
|
||||
"value": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Text value to type into the field. Required for action='fill'.",
|
||||
},
|
||||
},
|
||||
"required": []string{"action"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *BrowserTool) Execute(_ context.Context, input []byte, configuration string) ([]byte, error) {
|
||||
|
||||
slog.Debug("[browser]", "input", string(input))
|
||||
|
||||
var params struct {
|
||||
Action string `json:"action"`
|
||||
URL string `json:"url"`
|
||||
Selector string `json:"selector"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
if err := json.Unmarshal(input, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
var toolConfig BrowserConfig
|
||||
err := json.Unmarshal([]byte(configuration), &toolConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeout := time.Duration(toolConfig.TimeoutSeconds) * time.Second
|
||||
|
||||
// Headless est nil quand la configuration ne le précise pas : défaut à true.
|
||||
headless := true
|
||||
if toolConfig.Headless != nil {
|
||||
headless = *toolConfig.Headless
|
||||
}
|
||||
|
||||
if timeout <= 0 {
|
||||
timeout = 60 * time.Second
|
||||
}
|
||||
|
||||
browserMu.Lock()
|
||||
defer browserMu.Unlock()
|
||||
|
||||
if err := ensureBrowser(headless); err != nil {
|
||||
return nil, fmt.Errorf("browser init failed: %w", err)
|
||||
}
|
||||
|
||||
switch params.Action {
|
||||
case "navigate":
|
||||
if params.URL == "" {
|
||||
return nil, fmt.Errorf("'url' is required for action 'navigate'")
|
||||
}
|
||||
return t.navigate(params.URL, timeout)
|
||||
|
||||
case "click":
|
||||
if params.Selector == "" {
|
||||
return nil, fmt.Errorf("'selector' is required for action 'click'")
|
||||
}
|
||||
return t.click(params.Selector, timeout)
|
||||
|
||||
case "fill":
|
||||
if params.Selector == "" || params.Value == "" {
|
||||
return nil, fmt.Errorf("'selector' and 'value' are required for action 'fill'")
|
||||
}
|
||||
return t.fill(params.Selector, params.Value, timeout)
|
||||
|
||||
case "content":
|
||||
return t.pageContent(timeout)
|
||||
|
||||
case "links":
|
||||
return t.pageLinks(timeout)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown action %q", params.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func browserProfileDir() string {
|
||||
if dir, err := os.UserCacheDir(); err == nil {
|
||||
return filepath.Join(dir, "lassistanoque", "browser-profile")
|
||||
}
|
||||
return filepath.Join(os.TempDir(), "lassistanoque-browser-profile")
|
||||
}
|
||||
|
||||
// ensureBrowser lance le navigateur au premier appel et le relance si le mode
|
||||
// headless a changé dans la configuration.
|
||||
func ensureBrowser(headless bool) error {
|
||||
if browserInstance != nil {
|
||||
if headless == launchedHeadless {
|
||||
return nil
|
||||
}
|
||||
_ = browserInstance.Close()
|
||||
browserInstance = nil
|
||||
currentPage = nil
|
||||
}
|
||||
if err := initBrowser(headless); err != nil {
|
||||
return err
|
||||
}
|
||||
launchedHeadless = headless
|
||||
return nil
|
||||
}
|
||||
|
||||
func initBrowser(headless bool) error {
|
||||
path, _ := launcher.LookPath()
|
||||
controlURL, err := launcher.New().
|
||||
Bin(path).
|
||||
Headless(headless).
|
||||
UserDataDir(browserProfileDir()).
|
||||
Delete("enable-automation").
|
||||
Launch()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to launch browser: %w", err)
|
||||
}
|
||||
b := rod.New().ControlURL(controlURL)
|
||||
if err := b.Connect(); err != nil {
|
||||
return fmt.Errorf("failed to connect to browser: %w", err)
|
||||
}
|
||||
browserInstance = b
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensurePage(timeout time.Duration) error {
|
||||
if currentPage == nil {
|
||||
page, err := stealth.Page(browserInstance.Timeout(timeout))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open page: %w", err)
|
||||
}
|
||||
currentPage = page
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *BrowserTool) navigate(rawURL string, timeout time.Duration) ([]byte, error) {
|
||||
if err := ensurePage(timeout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := currentPage.Timeout(timeout).Navigate(rawURL); err != nil {
|
||||
return nil, fmt.Errorf("navigation failed: %w", err)
|
||||
}
|
||||
if err := currentPage.Timeout(timeout).WaitLoad(); err != nil {
|
||||
return nil, fmt.Errorf("page load failed: %w", err)
|
||||
}
|
||||
return t.pageContent(timeout)
|
||||
}
|
||||
|
||||
func (t *BrowserTool) click(selector string, timeout time.Duration) ([]byte, error) {
|
||||
if err := ensurePage(timeout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
el, err := currentPage.Timeout(timeout).Element(selector)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("element %q not found: %w", selector, err)
|
||||
}
|
||||
if err := el.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||
return nil, fmt.Errorf("click failed: %w", err)
|
||||
}
|
||||
if err := currentPage.WaitIdle(timeout); err != nil {
|
||||
return nil, fmt.Errorf("wait after click failed: %w", err)
|
||||
}
|
||||
return t.pageContent(timeout)
|
||||
}
|
||||
|
||||
func (t *BrowserTool) fill(selector, value string, timeout time.Duration) ([]byte, error) {
|
||||
if err := ensurePage(timeout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
el, err := currentPage.Timeout(timeout).Element(selector)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("element %q not found: %w", selector, err)
|
||||
}
|
||||
if err := el.SelectAllText(); err != nil {
|
||||
return nil, fmt.Errorf("failed to select text: %w", err)
|
||||
}
|
||||
if err := el.Input(value); err != nil {
|
||||
return nil, fmt.Errorf("fill failed: %w", err)
|
||||
}
|
||||
return t.pageContent(timeout)
|
||||
}
|
||||
|
||||
func (t *BrowserTool) pageContent(timeout time.Duration) ([]byte, error) {
|
||||
if currentPage == nil {
|
||||
return []byte("No page loaded."), nil
|
||||
}
|
||||
|
||||
info, err := currentPage.Info()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get page info: %w", err)
|
||||
}
|
||||
|
||||
titleRes, err := currentPage.Eval("() => document.title")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get page title: %w", err)
|
||||
}
|
||||
title := titleRes.Value.String()
|
||||
|
||||
body, err := currentPage.Timeout(timeout).Element("body")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get page body: %w", err)
|
||||
}
|
||||
text, err := body.Text()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract page text: %w", err)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "Page: %s\nURL: %s\n\n%s", title, info.URL, text)
|
||||
return []byte(sb.String()), nil
|
||||
}
|
||||
|
||||
func (t *BrowserTool) pageLinks(timeout time.Duration) ([]byte, error) {
|
||||
if currentPage == nil {
|
||||
return []byte("No page loaded."), nil
|
||||
}
|
||||
|
||||
elements, err := currentPage.Timeout(timeout).Elements("a[href]")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get links: %w", err)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "Links on current page (%d):\n\n", len(elements))
|
||||
for i, el := range elements {
|
||||
href, _ := el.Attribute("href")
|
||||
text, _ := el.Text()
|
||||
if href == nil {
|
||||
continue
|
||||
}
|
||||
label := strings.TrimSpace(text)
|
||||
if label == "" {
|
||||
label = "(no text)"
|
||||
}
|
||||
fmt.Fprintf(&sb, "%d. %s — %s\n", i+1, label, *href)
|
||||
}
|
||||
return []byte(sb.String()), nil
|
||||
}
|
||||
@@ -15,6 +15,7 @@ type Chat struct {
|
||||
UserID string `db:"user_id" json:"userId"`
|
||||
TaskID string `db:"task_id" json:"taskId,omitempty"`
|
||||
Title string `db:"title" json:"title"`
|
||||
FreshTitle bool `db:"fresh_title" json:"freshTitle"`
|
||||
DateCreated time.Time `db:"_date_created" json:"_date_created"`
|
||||
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
|
||||
VersionId string `db:"_version" json:"_version"`
|
||||
@@ -139,10 +140,17 @@ type LLMEngine interface {
|
||||
params *LLMParams,
|
||||
messages []*Message,
|
||||
) *Message
|
||||
Generate(
|
||||
ctx context.Context,
|
||||
provider *Provider,
|
||||
modelID string,
|
||||
messages []*Message,
|
||||
) (string, error)
|
||||
}
|
||||
|
||||
type ChatRepository interface {
|
||||
ListChats(userID string, teamID string, page int) ([]*Chat, error)
|
||||
ListChatsWithUnfreshTitles(teamID string) ([]*Chat, error)
|
||||
GetChat(userID string, teamID string, id string) (*Chat, error)
|
||||
CreateChat(userID string, chat *Chat) (*Chat, error)
|
||||
UpdateChat(userID string, chat *Chat) (*Chat, error)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -61,6 +62,9 @@ func (h *ChatHandler) NewChat(c *echo.Context) error {
|
||||
}
|
||||
|
||||
func (h *ChatHandler) NewChatMessage(c *echo.Context) error {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
userID := c.Get(auth.ContextUserIDKey).(string)
|
||||
teamID := c.Param("space")
|
||||
chatID := c.Param("id")
|
||||
@@ -75,7 +79,7 @@ func (h *ChatHandler) NewChatMessage(c *echo.Context) error {
|
||||
chunkChan := make(chan *domain.Chunk)
|
||||
|
||||
_, err = h.chatService.AddChatMessage(
|
||||
c.Request().Context(),
|
||||
ctx,
|
||||
userID,
|
||||
teamID,
|
||||
chatID,
|
||||
|
||||
@@ -146,6 +146,12 @@ func (s *Service) AddChatMessage(
|
||||
cb,
|
||||
)
|
||||
|
||||
chat, _ := s.repoChat.GetChat(userID, teamID, chatID)
|
||||
chat.FreshTitle = false
|
||||
s.repoChat.UpdateChat(userID, chat)
|
||||
|
||||
go s.RefreshTitles(provider, chatModelID, teamID)
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"trankilou.fr/lassistanoque/backend/internal/domain"
|
||||
)
|
||||
|
||||
func (s *Service) RefreshTitles(provider *domain.Provider, modelID string, teamID string) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
chats, err := s.repoChat.ListChatsWithUnfreshTitles(teamID)
|
||||
if err != nil {
|
||||
slog.Error("RefreshTitles.ListChats", "error", err)
|
||||
}
|
||||
for _, chat := range chats {
|
||||
newchat, _ := s.repoChat.GetChat(chat.UserID, chat.TeamID, chat.ID)
|
||||
|
||||
if !chat.FreshTitle {
|
||||
messages, err := s.repoChat.GetChatMessages(newchat.UserID, newchat.TeamID, newchat.ID, true)
|
||||
if err != nil {
|
||||
slog.Error("RefreshTitles.GetMessages", "error", err)
|
||||
}
|
||||
title, err := s.llmEngine.Generate(ctx, provider, modelID, append(messages, &domain.Message{
|
||||
Role: string(domain.RoleUser),
|
||||
Content: "resume toute la conversation dans un titre entre 5 et 10 mots",
|
||||
}))
|
||||
if err != nil {
|
||||
slog.Error("RefreshTitles.Generate", "error", err)
|
||||
}
|
||||
newchat.FreshTitle = true
|
||||
newchat.Title = title
|
||||
s.repoChat.UpdateChat(newchat.UserID, newchat)
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,24 +1,21 @@
|
||||
<script lang="ts">
|
||||
import {autofocus} from "$lib/ui/autofocus"
|
||||
import {onMount, tick } from "svelte"
|
||||
import {onMount, tick, untrack } from "svelte"
|
||||
import {page} from "$app/state"
|
||||
import {chatApi} from "$lib/api/endpoints/chat"
|
||||
import type { PageProps } from './$types';
|
||||
import type { Message, ChatWithMessages } from "$lib/types/api"
|
||||
import type { Message } from "$lib/types/api"
|
||||
import SvelteMarkdown from '@humanspeak/svelte-markdown'
|
||||
|
||||
let prompt = $state("")
|
||||
let { data }: PageProps = $props();
|
||||
let messages = $state([] as Message[])
|
||||
let messages = $state(untrack(()=>data.chatWithMessage.messages))
|
||||
let waiting = $state(false)
|
||||
|
||||
onMount(async ()=>{
|
||||
if (page.state.prompt) {
|
||||
console.log(page.state.prompt)
|
||||
await sendRequest(page.state.prompt)
|
||||
} else {
|
||||
let chatWithMessage : ChatWithMessages = await chatApi.getChat(data.spaceId, data.chatId)
|
||||
messages = chatWithMessage.messages
|
||||
}
|
||||
await tick()
|
||||
historyDiv.scrollTop = historyDiv.scrollHeight;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import { chatApi } from "$lib/api/endpoints/chat"
|
||||
|
||||
export const load: PageLoad = ({params}) => {
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
const chatWithMessage = await chatApi.getChat(params.space, params.conversation)
|
||||
return {
|
||||
title: 'Nouvelle conversation',
|
||||
title: chatWithMessage.chat.title,
|
||||
spaceId: params.space,
|
||||
chatId: params.conversation,
|
||||
chatWithMessage: chatWithMessage,
|
||||
};
|
||||
}
|
||||
@@ -21,11 +21,11 @@ let {
|
||||
|
||||
let tool = $state(untrack(()=>item))
|
||||
let toolParams = $state([] as ToolParam[])
|
||||
let paramValues = new SvelteMap<string, string>();
|
||||
let paramValues = new SvelteMap<string, unknown>();
|
||||
|
||||
onMount(async () => {
|
||||
toolParams = await toolApi.getToolParams(item.teamId, item.id)
|
||||
paramValues = new SvelteMap<string, string>(Object.entries(JSON.parse(item.configuration)));
|
||||
paramValues = new SvelteMap<string, unknown>(Object.entries(JSON.parse(item.configuration)));
|
||||
})
|
||||
|
||||
const autofocus = (node: HTMLElement) => {
|
||||
@@ -79,12 +79,29 @@ const handleCancel = (e: Event) => {
|
||||
|
||||
<div class="formelement">
|
||||
<label for={param.name}>{param.name}</label>
|
||||
<input
|
||||
id={param.name}
|
||||
bind:value={() => paramValues.get(param.name) ?? '', (v) => paramValues.set(param.name, v)}
|
||||
class="textinput"
|
||||
placeholder={param.description}
|
||||
/>
|
||||
{#if param.type == "bool"}
|
||||
<input
|
||||
id="enabled"
|
||||
type="checkbox"
|
||||
bind:checked={() => paramValues.get(param.name) as boolean ?? '', (v) => paramValues.set(param.name, v)}
|
||||
class="checkboxinput"
|
||||
/>
|
||||
{:else if param.type=="int"}
|
||||
<input
|
||||
id={param.name}
|
||||
type="number"
|
||||
bind:value={() => paramValues.get(param.name) ?? '', (v) => paramValues.set(param.name, v)}
|
||||
class="textinput"
|
||||
placeholder={param.description}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
id={param.name}
|
||||
bind:value={() => paramValues.get(param.name) ?? '', (v) => paramValues.set(param.name, v)}
|
||||
class="textinput"
|
||||
placeholder={param.description}
|
||||
/>
|
||||
{/if}
|
||||
<FormError key="baseUrl" errors={errors} />
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user