|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
{{template "views/partials/head" .}} |
|
|
|
|
|
<body class="bg-[#101827] text-[#E5E7EB]"> |
|
|
<div class="flex flex-col min-h-screen" x-data="taskDetails()" x-init="init()"> |
|
|
|
|
|
{{template "views/partials/navbar" .}} |
|
|
|
|
|
<div class="container mx-auto px-4 py-8 flex-grow max-w-6xl"> |
|
|
|
|
|
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8"> |
|
|
<div class="flex justify-between items-center"> |
|
|
<div> |
|
|
<h1 class="hero-title"> |
|
|
<span x-text="isNewTask ? 'Create Task' : (isEditMode ? 'Edit Task' : 'Task Details')"></span> |
|
|
</h1> |
|
|
<p class="text-lg text-[#94A3B8]" x-text="isNewTask ? 'Create a new agent task' : (task ? task.name : 'Loading...')"></p> |
|
|
</div> |
|
|
<div class="flex space-x-3"> |
|
|
<template x-if="!isNewTask && !isEditMode"> |
|
|
<div class="flex space-x-3"> |
|
|
<button @click="showExecuteModal()" |
|
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors"> |
|
|
<i class="fas fa-play mr-2"></i>Execute |
|
|
</button> |
|
|
<button @click="enterEditMode()" |
|
|
class="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-lg transition-colors"> |
|
|
<i class="fas fa-edit mr-2"></i>Edit |
|
|
</button> |
|
|
<button @click="deleteTask()" |
|
|
class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors"> |
|
|
<i class="fas fa-trash mr-2"></i>Delete |
|
|
</button> |
|
|
</div> |
|
|
</template> |
|
|
<template x-if="isEditMode || isNewTask"> |
|
|
<div class="flex space-x-3"> |
|
|
<button @click="cancelEdit()" |
|
|
class="bg-[#1E293B] hover:bg-[#2D3A4F] text-white px-4 py-2 rounded-lg transition-colors"> |
|
|
Cancel |
|
|
</button> |
|
|
<button @click="saveTask()" |
|
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors"> |
|
|
<i class="fas fa-save mr-2"></i>Save |
|
|
</button> |
|
|
</div> |
|
|
</template> |
|
|
<a href="/agent-jobs" class="text-[#94A3B8] hover:text-[#E5E7EB] px-4 py-2"> |
|
|
<i class="fas fa-arrow-left mr-2"></i>Back |
|
|
</a> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<template x-if="isEditMode || isNewTask"> |
|
|
<form @submit.prevent="saveTask()" class="space-y-8"> |
|
|
|
|
|
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8"> |
|
|
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Basic Information</h2> |
|
|
|
|
|
<div class="space-y-6"> |
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">Name *</label> |
|
|
<input type="text" x-model="taskForm.name" required |
|
|
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"> |
|
|
</div> |
|
|
|
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">Description</label> |
|
|
<textarea x-model="taskForm.description" rows="3" |
|
|
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> |
|
|
</div> |
|
|
|
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">Model *</label> |
|
|
<select x-model="taskForm.model" required |
|
|
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"> |
|
|
<option value="">Select a model with MCP configuration...</option> |
|
|
{{ range .ModelsConfig }} |
|
|
{{ $cfg := . }} |
|
|
{{ $hasMCP := or (ne $cfg.MCP.Servers "") (ne $cfg.MCP.Stdio "") }} |
|
|
{{ if $hasMCP }} |
|
|
<option value="{{$cfg.Name}}" class="bg-[#1E293B] text-[#E5E7EB]">{{$cfg.Name}}</option> |
|
|
{{ end }} |
|
|
{{ end }} |
|
|
</select> |
|
|
<p class="text-sm text-[#94A3B8] mt-1">Only models with MCP configuration are shown</p> |
|
|
</div> |
|
|
|
|
|
<div> |
|
|
<label class="flex items-center"> |
|
|
<input type="checkbox" x-model="taskForm.enabled" |
|
|
class="mr-2"> |
|
|
<span class="text-[#E5E7EB]">Enabled</span> |
|
|
</label> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8"> |
|
|
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Prompt Template</h2> |
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">Prompt *</label> |
|
|
<p class="text-sm text-[#94A3B8] mb-4"> |
|
|
Use Go template syntax with <code class="bg-[#101827] px-1.5 py-0.5 rounded text-[#38BDF8]">{{"{{"}}.param{{"}}"}}</code> for dynamic parameters. |
|
|
Parameters are provided when executing the job and will be substituted into the prompt. |
|
|
</p> |
|
|
|
|
|
|
|
|
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4 mb-4"> |
|
|
<p class="text-xs text-[#94A3B8] mb-2 font-semibold">Example Prompt:</p> |
|
|
<pre class="text-xs text-[#E5E7EB] font-mono whitespace-pre-wrap">You are a helpful assistant. The user's name is {{"{{"}}.user_name{{"}}"}} and they work as a {{"{{"}}.job_title{{"}}"}}. |
|
|
|
|
|
Please help them with the following task: {{"{{"}}.task_description{{"}}"}} |
|
|
|
|
|
Provide a detailed response that addresses their specific needs.</pre> |
|
|
</div> |
|
|
|
|
|
<textarea x-model="taskForm.prompt" required rows="12" |
|
|
placeholder="Enter your prompt template here. Use {{.parameter_name}} to reference parameters that will be provided when the job executes." |
|
|
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> |
|
|
<p class="text-xs text-[#94A3B8] mt-2"> |
|
|
<i class="fas fa-info-circle mr-1"></i> |
|
|
The prompt will be processed as a Go template. All parameters passed during job execution will be available as template variables. |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8"> |
|
|
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Cron Schedule (Optional)</h2> |
|
|
<div class="space-y-6"> |
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">Cron Expression</label> |
|
|
<input type="text" |
|
|
x-model="taskForm.cron" |
|
|
@blur="validateCron(taskForm.cron)" |
|
|
@input="cronError = ''" |
|
|
placeholder="0 0 * * * (daily at midnight)" |
|
|
:class="cronError ? 'w-full bg-[#101827] border border-red-500 rounded px-4 py-2 text-[#E5E7EB] focus:border-red-500 focus:ring-2 focus:ring-red-500/50' : 'w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50'"> |
|
|
<p class="text-sm text-[#94A3B8] mt-1">Standard 5-field cron format (minute hour day month weekday)</p> |
|
|
<p x-show="cronError" class="text-sm text-red-400 mt-2" x-text="cronError"></p> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">Cron Parameters (Optional)</label> |
|
|
<p class="text-sm text-[#94A3B8] mb-3"> |
|
|
Parameters to use when executing jobs triggered by cron. These will be used to template the prompt. |
|
|
Enter as key-value pairs (one per line, format: key=value). |
|
|
</p> |
|
|
<textarea x-model="cronParametersText" |
|
|
@input="updateCronParameters()" |
|
|
rows="6" |
|
|
placeholder="user_name=Alice job_title=Software Engineer task_description=Daily status report" |
|
|
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> |
|
|
<p class="text-xs text-[#94A3B8] mt-1"> |
|
|
<i class="fas fa-info-circle mr-1"></i> |
|
|
Example: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">user_name=Alice</code> |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8"> |
|
|
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Multimedia Sources (Optional)</h2> |
|
|
<p class="text-sm text-[#94A3B8] mb-4"> |
|
|
Configure multimedia sources (images, videos, audios, files) to fetch when cron jobs execute. |
|
|
Each source can have custom headers for authentication/authorization. These will be fetched and included in the job execution. |
|
|
</p> |
|
|
<div class="space-y-4"> |
|
|
<template x-for="(source, index) in taskForm.multimedia_sources" :key="index"> |
|
|
<div class="bg-[#101827] p-4 rounded border border-[#38BDF8]/10"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h3 class="text-lg font-semibold text-[#E5E7EB]">Multimedia Source <span x-text="index + 1"></span></h3> |
|
|
<button type="button" @click="taskForm.multimedia_sources.splice(index, 1)" |
|
|
class="text-red-400 hover:text-red-300"> |
|
|
<i class="fas fa-trash"></i> |
|
|
</button> |
|
|
</div> |
|
|
<div class="space-y-4"> |
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">Type *</label> |
|
|
<select x-model="source.type" required |
|
|
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"> |
|
|
<option value="">Select type...</option> |
|
|
<option value="image">Image</option> |
|
|
<option value="video">Video</option> |
|
|
<option value="audio">Audio</option> |
|
|
<option value="file">File</option> |
|
|
</select> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">URL *</label> |
|
|
<input type="url" x-model="source.url" required |
|
|
placeholder="https://example.com/image.png" |
|
|
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"> |
|
|
<p class="text-xs text-[#94A3B8] mt-1">URL where multimedia content will be fetched from</p> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">Headers (JSON)</label> |
|
|
<textarea x-model="source.headers_json" rows="3" |
|
|
placeholder='{"Authorization": "Bearer token"}' |
|
|
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> |
|
|
<p class="text-xs text-[#94A3B8] mt-1">Custom headers for the HTTP request (e.g., Authorization)</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
<button type="button" @click="addMultimediaSource()" |
|
|
class="w-full bg-[#101827] hover:bg-[#0A0E1A] border border-[#38BDF8]/20 border-dashed rounded-lg p-4 text-[#94A3B8] hover:text-[#E5E7EB] transition-colors"> |
|
|
<i class="fas fa-plus mr-2"></i>Add Multimedia Source |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8"> |
|
|
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Webhooks (Optional)</h2> |
|
|
<p class="text-sm text-[#94A3B8] mb-4"> |
|
|
Configure webhook URLs to receive notifications when jobs complete. You can add multiple webhooks, each with custom headers and HTTP methods. |
|
|
</p> |
|
|
<div class="space-y-4"> |
|
|
<template x-for="(webhook, index) in taskForm.webhooks" :key="index"> |
|
|
<div class="bg-[#101827] p-4 rounded border border-[#38BDF8]/10"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h3 class="text-lg font-semibold text-[#E5E7EB]">Webhook <span x-text="index + 1"></span></h3> |
|
|
<button type="button" @click="taskForm.webhooks.splice(index, 1)" |
|
|
class="text-red-400 hover:text-red-300"> |
|
|
<i class="fas fa-trash"></i> |
|
|
</button> |
|
|
</div> |
|
|
<div class="space-y-4"> |
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">URL *</label> |
|
|
<input type="url" x-model="webhook.url" required |
|
|
placeholder="https://hooks.slack.com/services/YOUR/WEBHOOK/URL" |
|
|
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"> |
|
|
<p class="text-xs text-[#94A3B8] mt-1">URL where webhook notifications will be sent</p> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">HTTP Method</label> |
|
|
<select x-model="webhook.method" |
|
|
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"> |
|
|
<option value="POST">POST</option> |
|
|
<option value="PUT">PUT</option> |
|
|
<option value="PATCH">PATCH</option> |
|
|
</select> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">Headers (JSON)</label> |
|
|
<textarea x-model="webhook.headers_json" rows="3" |
|
|
placeholder='{"Authorization": "Bearer token", "Content-Type": "application/json"}' |
|
|
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> |
|
|
<p class="text-xs text-[#94A3B8] mt-1">Custom headers for the webhook request (e.g., Authorization)</p> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-[#E5E7EB] mb-2">Custom Payload Template (Optional)</label> |
|
|
<p class="text-xs text-[#94A3B8] mb-2">Customize the webhook payload using Go template syntax. Available variables: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Job</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Task</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Result</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Error</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Status</code></p> |
|
|
<p class="text-xs text-[#94A3B8] mb-2">Note: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Error</code> will be empty string if job succeeded, or contain the error message if it failed. Use this to handle both success and failure cases in a single webhook.</p> |
|
|
<div class="bg-[#0A0E1A] border border-[#38BDF8]/10 rounded-lg p-3 mb-2"> |
|
|
<p class="text-xs text-[#94A3B8] mb-1 font-semibold">Example (Slack with error handling):</p> |
|
|
<pre class="text-xs text-[#E5E7EB] font-mono whitespace-pre-wrap">{ |
|
|
"text": "Job {{.Job.ID}} {{if .Error}}failed{{else}}completed{{end}}", |
|
|
"blocks": [ |
|
|
{ |
|
|
"type": "section", |
|
|
"text": { |
|
|
"type": "mrkdwn", |
|
|
"text": "*Task:* {{.Task.Name}}\n*Status:* {{.Status}}\n{{if .Error}}*Error:* {{.Error}}{{else}}*Result:* {{.Result}}{{end}}" |
|
|
} |
|
|
} |
|
|
] |
|
|
}</pre> |
|
|
</div> |
|
|
<textarea x-model="webhook.payload_template" rows="5" |
|
|
placeholder='{"text": "Job {{.Job.ID}} completed with status {{.Status}}", "error": "{{.Error}}"}' |
|
|
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
<button type="button" @click="addWebhook()" |
|
|
class="w-full bg-[#101827] border border-[#38BDF8]/20 hover:border-[#38BDF8]/40 rounded px-4 py-3 text-[#38BDF8] transition-colors"> |
|
|
<i class="fas fa-plus mr-2"></i>Add Webhook |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
</form> |
|
|
</template> |
|
|
|
|
|
|
|
|
<div x-show="!isEditMode && !isNewTask" x-cloak> |
|
|
|
|
|
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8"> |
|
|
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Task Information</h2> |
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
|
|
<div> |
|
|
<label class="text-[#94A3B8] text-sm">Name</label> |
|
|
<div class="text-[#E5E7EB] mt-1 font-semibold" x-text="task ? task.name : 'Loading...'"></div> |
|
|
</div> |
|
|
<div> |
|
|
<label class="text-[#94A3B8] text-sm">Status</label> |
|
|
<div class="mt-1"> |
|
|
<span :class="task && task.enabled ? 'bg-green-500' : 'bg-gray-500'" |
|
|
class="px-2 py-1 rounded text-xs text-white" |
|
|
x-text="task && task.enabled ? 'Enabled' : 'Disabled'"></span> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<label class="text-[#94A3B8] text-sm">Model</label> |
|
|
<div class="mt-1 flex items-center space-x-2"> |
|
|
<a :href="task ? '/chat/' + task.model + '?mcp=true' : '#'" |
|
|
class="text-[#38BDF8] hover:text-[#38BDF8]/80 hover:underline" |
|
|
x-text="task ? task.model : '-'"></a> |
|
|
<a :href="task ? '/models/edit/' + task.model : '#'" |
|
|
class="text-yellow-400 hover:text-yellow-300" |
|
|
title="Edit model configuration"> |
|
|
<i class="fas fa-edit text-sm"></i> |
|
|
</a> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<label class="text-[#94A3B8] text-sm">Cron Schedule</label> |
|
|
<div class="text-[#E5E7EB] mt-1 font-mono text-sm" x-text="task && task.cron ? task.cron : '-'"></div> |
|
|
</div> |
|
|
<div class="md:col-span-2" x-show="task && task.cron_parameters && Object.keys(task.cron_parameters).length > 0"> |
|
|
<label class="text-[#94A3B8] text-sm">Cron Parameters</label> |
|
|
<div class="mt-1"> |
|
|
<template x-for="(value, key) in task.cron_parameters" :key="key"> |
|
|
<div class="text-[#E5E7EB] text-sm mb-1"> |
|
|
<span class="font-semibold text-[#38BDF8]" x-text="key + ':'"></span> |
|
|
<span x-text="value"></span> |
|
|
</div> |
|
|
</template> |
|
|
</div> |
|
|
</div> |
|
|
<div class="md:col-span-2"> |
|
|
<label class="text-[#94A3B8] text-sm">Description</label> |
|
|
<div class="text-[#E5E7EB] mt-1" x-text="task && task.description ? task.description : 'No description'"></div> |
|
|
</div> |
|
|
<div class="md:col-span-2"> |
|
|
<label class="text-[#94A3B8] text-sm">Prompt Template</label> |
|
|
<pre class="bg-[#101827] p-4 rounded text-[#E5E7EB] text-sm mt-1 whitespace-pre-wrap" x-text="task ? task.prompt : '-'"></pre> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="task && task.id"> |
|
|
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">API Usage Examples</h2> |
|
|
<p class="text-sm text-[#94A3B8] mb-4"> |
|
|
Use these curl commands to interact with this task programmatically. |
|
|
</p> |
|
|
|
|
|
<div class="space-y-6"> |
|
|
|
|
|
<div> |
|
|
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center"> |
|
|
<i class="fas fa-play text-[#38BDF8] mr-2"></i> |
|
|
Execute Task by ID |
|
|
</h3> |
|
|
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4"> |
|
|
<pre class="text-xs text-[#E5E7EB] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/jobs/execute \ |
|
|
-H "Content-Type: application/json" \ |
|
|
-H "Authorization: Bearer YOUR_API_KEY" \ |
|
|
-d '{ |
|
|
"task_id": "<span x-text="task ? task.id : 'task-uuid'"></span>", |
|
|
"parameters": { |
|
|
"user_name": "Alice", |
|
|
"job_title": "Software Engineer", |
|
|
"task_description": "Review code changes" |
|
|
} |
|
|
}'</code></pre> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center"> |
|
|
<i class="fas fa-code text-[#38BDF8] mr-2"></i> |
|
|
Execute Task by Name |
|
|
</h3> |
|
|
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4"> |
|
|
<pre class="text-xs text-[#E5E7EB] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/tasks/<span x-text="task ? task.name : 'task-name'"></span>/execute \ |
|
|
-H "Content-Type: application/json" \ |
|
|
-H "Authorization: Bearer YOUR_API_KEY" \ |
|
|
-d '{ |
|
|
"user_name": "Bob", |
|
|
"job_title": "Data Scientist", |
|
|
"task_description": "Analyze sales data" |
|
|
}'</code></pre> |
|
|
</div> |
|
|
<p class="text-xs text-[#94A3B8] mt-2"> |
|
|
<i class="fas fa-info-circle mr-1"></i> |
|
|
The request body should be a JSON object where keys are parameter names and values are strings. |
|
|
If no body is provided, the task will execute with empty parameters. |
|
|
</p> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center"> |
|
|
<i class="fas fa-images text-[#38BDF8] mr-2"></i> |
|
|
Execute Task with Multimedia (Images) |
|
|
</h3> |
|
|
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4"> |
|
|
<pre class="text-xs text-[#E5E7EB] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/jobs/execute \ |
|
|
-H "Content-Type: application/json" \ |
|
|
-H "Authorization: Bearer YOUR_API_KEY" \ |
|
|
-d '{ |
|
|
"task_id": "<span x-text="task ? task.id : 'task-uuid'"></span>", |
|
|
"parameters": { |
|
|
"user_name": "Alice", |
|
|
"task_description": "Analyze this image" |
|
|
}, |
|
|
"images": [ |
|
|
"https://example.com/image.png", |
|
|
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" |
|
|
] |
|
|
}'</code></pre> |
|
|
</div> |
|
|
<p class="text-xs text-[#94A3B8] mt-2"> |
|
|
You can provide multimedia content as URLs or base64-encoded data URIs. Supported types: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">images</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">videos</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">audios</code>, and <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">files</code>. |
|
|
</p> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center"> |
|
|
<i class="fas fa-info-circle text-[#38BDF8] mr-2"></i> |
|
|
Check Job Status |
|
|
</h3> |
|
|
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4"> |
|
|
<pre class="text-xs text-[#E5E7EB] font-mono overflow-x-auto"><code>curl -X GET {{ .BaseURL }}api/agent/jobs/JOB_ID \ |
|
|
-H "Authorization: Bearer YOUR_API_KEY"</code></pre> |
|
|
</div> |
|
|
<p class="text-xs text-[#94A3B8] mt-2"> |
|
|
After executing a task, you will receive a <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">job_id</code> in the response. Use it to query the job's status and results. |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="task && task.id && task.webhooks && task.webhooks.length > 0"> |
|
|
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Webhook Configuration</h2> |
|
|
<div class="space-y-4"> |
|
|
<template x-for="(webhook, index) in task.webhooks" :key="index"> |
|
|
<div class="bg-[#101827] p-4 rounded border border-[#38BDF8]/10"> |
|
|
<div class="flex items-center mb-3"> |
|
|
<h3 class="text-lg font-semibold text-[#E5E7EB]">Webhook <span x-text="index + 1"></span></h3> |
|
|
</div> |
|
|
<div class="space-y-3"> |
|
|
<div> |
|
|
<label class="text-[#94A3B8] text-sm">URL</label> |
|
|
<div class="text-[#E5E7EB] mt-1 font-mono text-sm break-all" x-text="webhook.url"></div> |
|
|
</div> |
|
|
<div> |
|
|
<label class="text-[#94A3B8] text-sm">Method</label> |
|
|
<div class="text-[#E5E7EB] mt-1 font-mono text-sm" x-text="webhook.method || 'POST'"></div> |
|
|
</div> |
|
|
<div x-show="webhook.headers && Object.keys(webhook.headers).length > 0"> |
|
|
<label class="text-[#94A3B8] text-sm">Headers</label> |
|
|
<pre class="bg-[#0A0E1A] p-3 rounded text-[#E5E7EB] text-xs mt-1 overflow-x-auto" x-text="JSON.stringify(webhook.headers, null, 2)"></pre> |
|
|
</div> |
|
|
<div x-show="webhook.payload_template"> |
|
|
<label class="text-[#94A3B8] text-sm">Payload Template</label> |
|
|
<pre class="bg-[#0A0E1A] p-3 rounded text-[#E5E7EB] text-xs mt-1 whitespace-pre-wrap overflow-x-auto" x-text="webhook.payload_template"></pre> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<template x-if="!isNewTask && !isEditMode"> |
|
|
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8"> |
|
|
<div class="flex justify-between items-center mb-6"> |
|
|
<h2 class="text-2xl font-semibold text-[#E5E7EB]">Job History</h2> |
|
|
<div class="flex space-x-4"> |
|
|
<select x-model="jobFilter" @change="fetchJobs()" |
|
|
class="bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB]"> |
|
|
<option value="">All Status</option> |
|
|
<option value="pending">Pending</option> |
|
|
<option value="running">Running</option> |
|
|
<option value="completed">Completed</option> |
|
|
<option value="failed">Failed</option> |
|
|
<option value="cancelled">Cancelled</option> |
|
|
</select> |
|
|
<button @click="clearJobHistory()" |
|
|
class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors" |
|
|
title="Clear all job history for this task"> |
|
|
<i class="fas fa-trash mr-2"></i>Clear History |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="overflow-x-auto"> |
|
|
<table class="w-full"> |
|
|
<thead> |
|
|
<tr class="border-b border-[#38BDF8]/20"> |
|
|
<th class="text-left py-3 px-4 text-[#94A3B8]">Job ID</th> |
|
|
<th class="text-left py-3 px-4 text-[#94A3B8]">Status</th> |
|
|
<th class="text-left py-3 px-4 text-[#94A3B8]">Created</th> |
|
|
<th class="text-left py-3 px-4 text-[#94A3B8]">Triggered By</th> |
|
|
<th class="text-left py-3 px-4 text-[#94A3B8]">Actions</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
<template x-for="job in jobs" :key="job.id"> |
|
|
<tr class="border-b border-[#38BDF8]/10 hover:bg-[#101827]"> |
|
|
<td class="py-3 px-4"> |
|
|
<a :href="'/agent-jobs/jobs/' + job.id" |
|
|
class="font-mono text-sm text-[#38BDF8] hover:text-[#38BDF8]/80 hover:underline" |
|
|
x-text="job.id.substring(0, 8) + '...'" |
|
|
:title="job.id"></a> |
|
|
</td> |
|
|
<td class="py-3 px-4"> |
|
|
<span :class="{ |
|
|
'bg-yellow-500': job.status === 'pending', |
|
|
'bg-blue-500': job.status === 'running', |
|
|
'bg-green-500': job.status === 'completed', |
|
|
'bg-red-500': job.status === 'failed', |
|
|
'bg-gray-500': job.status === 'cancelled' |
|
|
}" |
|
|
class="px-2 py-1 rounded text-xs text-white" |
|
|
x-text="job.status"></span> |
|
|
</td> |
|
|
<td class="py-3 px-4 text-[#94A3B8] text-sm" x-text="formatDate(job.created_at)"></td> |
|
|
<td class="py-3 px-4 text-[#94A3B8] text-sm" x-text="job.triggered_by || '-'"></td> |
|
|
<td class="py-3 px-4"> |
|
|
<button x-show="job.status === 'pending' || job.status === 'running'" |
|
|
@click="cancelJob(job.id)" |
|
|
class="text-red-400 hover:text-red-300" |
|
|
title="Cancel job"> |
|
|
<i class="fas fa-stop"></i> |
|
|
</button> |
|
|
</td> |
|
|
</tr> |
|
|
</template> |
|
|
<tr x-show="jobs.length === 0"> |
|
|
<td colspan="5" class="py-8 text-center text-[#94A3B8]">No jobs found for this task</td> |
|
|
</tr> |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div x-show="showExecuteTaskModal" |
|
|
x-cloak |
|
|
@click.away="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'" |
|
|
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> |
|
|
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl max-w-2xl w-full mx-4 max-h-[90vh] flex flex-col" @click.stop> |
|
|
<div class="flex justify-between items-center p-8 pb-6 border-b border-[#38BDF8]/20"> |
|
|
<h3 class="text-2xl font-semibold text-[#E5E7EB]">Execute Task</h3> |
|
|
<button @click="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'" |
|
|
class="text-[#94A3B8] hover:text-[#E5E7EB]"> |
|
|
<i class="fas fa-times text-xl"></i> |
|
|
</button> |
|
|
</div> |
|
|
<template x-if="task"> |
|
|
<div class="flex flex-col flex-1 min-h-0"> |
|
|
<div class="flex-1 overflow-y-auto px-8 py-6 space-y-4"> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Task</label> |
|
|
<div class="text-[#94A3B8]" x-text="task.name"></div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="border-b border-[#38BDF8]/20"> |
|
|
<div class="flex space-x-4"> |
|
|
<button @click="executeModalTab = 'parameters'" |
|
|
:class="executeModalTab === 'parameters' ? 'border-b-2 border-[#38BDF8] text-[#38BDF8]' : 'text-[#94A3B8] hover:text-[#E5E7EB]'" |
|
|
class="px-4 py-2 font-medium transition-colors"> |
|
|
Parameters |
|
|
</button> |
|
|
<button @click="executeModalTab = 'multimedia'" |
|
|
:class="executeModalTab === 'multimedia' ? 'border-b-2 border-[#38BDF8] text-[#38BDF8]' : 'text-[#94A3B8] hover:text-[#E5E7EB]'" |
|
|
class="px-4 py-2 font-medium transition-colors"> |
|
|
Multimedia |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div x-show="executeModalTab === 'parameters'"> |
|
|
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Parameters</label> |
|
|
<p class="text-xs text-[#94A3B8] mb-3"> |
|
|
Enter parameters as key-value pairs (one per line, format: key=value). |
|
|
These will be used to template the prompt. |
|
|
</p> |
|
|
<textarea x-model="executionParametersText" |
|
|
rows="6" |
|
|
placeholder="user_name=Alice job_title=Software Engineer task_description=Review code changes" |
|
|
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> |
|
|
<p class="text-xs text-[#94A3B8] mt-1"> |
|
|
Example: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">user_name=Alice</code> |
|
|
</p> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div x-show="executeModalTab === 'multimedia'" class="space-y-4"> |
|
|
<p class="text-xs text-[#94A3B8] mb-3"> |
|
|
Provide multimedia content as URLs or base64-encoded data URIs. You can also upload files which will be converted to base64. |
|
|
</p> |
|
|
|
|
|
|
|
|
<div> |
|
|
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Images</label> |
|
|
<textarea x-model="executionMultimedia.images" |
|
|
rows="3" |
|
|
placeholder="https://example.com/image.png data:image/png;base64,iVBORw0KG..." |
|
|
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> |
|
|
<input type="file" @change="handleFileUpload($event, 'image')" accept="image/*" multiple |
|
|
class="mt-2 text-sm text-[#94A3B8] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#38BDF8] file:text-white hover:file:bg-[#38BDF8]/80"> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Videos</label> |
|
|
<textarea x-model="executionMultimedia.videos" |
|
|
rows="3" |
|
|
placeholder="https://example.com/video.mp4 data:video/mp4;base64,..." |
|
|
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> |
|
|
<input type="file" @change="handleFileUpload($event, 'video')" accept="video/*" multiple |
|
|
class="mt-2 text-sm text-[#94A3B8] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#38BDF8] file:text-white hover:file:bg-[#38BDF8]/80"> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Audios</label> |
|
|
<textarea x-model="executionMultimedia.audios" |
|
|
rows="3" |
|
|
placeholder="https://example.com/audio.mp3 data:audio/mpeg;base64,..." |
|
|
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> |
|
|
<input type="file" @change="handleFileUpload($event, 'audio')" accept="audio/*" multiple |
|
|
class="mt-2 text-sm text-[#94A3B8] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#38BDF8] file:text-white hover:file:bg-[#38BDF8]/80"> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Files</label> |
|
|
<textarea x-model="executionMultimedia.files" |
|
|
rows="3" |
|
|
placeholder="https://example.com/file.pdf data:application/pdf;base64,..." |
|
|
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> |
|
|
<input type="file" @change="handleFileUpload($event, 'file')" multiple |
|
|
class="mt-2 text-sm text-[#94A3B8] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#38BDF8] file:text-white hover:file:bg-[#38BDF8]/80"> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="flex justify-end space-x-4 p-8 pt-6 border-t border-[#38BDF8]/20 bg-[#1E293B]"> |
|
|
<button @click="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'" |
|
|
class="px-4 py-2 bg-[#101827] hover:bg-[#0A0E1A] text-[#E5E7EB] rounded-lg transition-colors"> |
|
|
Cancel |
|
|
</button> |
|
|
<button @click="executeTaskWithParameters()" |
|
|
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"> |
|
|
<i class="fas fa-play mr-2"></i>Execute |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
function taskDetails() { |
|
|
return { |
|
|
taskId: null, |
|
|
task: null, |
|
|
jobs: [], |
|
|
jobFilter: '', |
|
|
showExecuteTaskModal: false, |
|
|
executionParameters: {}, |
|
|
executionParametersText: '', |
|
|
executionMultimedia: { |
|
|
images: '', |
|
|
videos: '', |
|
|
audios: '', |
|
|
files: '' |
|
|
}, |
|
|
executeModalTab: 'parameters', |
|
|
isNewTask: false, |
|
|
isEditMode: false, |
|
|
taskForm: { |
|
|
name: '', |
|
|
description: '', |
|
|
model: '', |
|
|
prompt: '', |
|
|
enabled: true, |
|
|
cron: '', |
|
|
cron_parameters: {}, |
|
|
webhooks: [], |
|
|
multimedia_sources: [] |
|
|
}, |
|
|
cronError: '', |
|
|
cronParametersText: '', |
|
|
|
|
|
init() { |
|
|
|
|
|
const path = window.location.pathname; |
|
|
if (path === '/agent-jobs/tasks/new') { |
|
|
this.isNewTask = true; |
|
|
this.taskId = null; |
|
|
} else { |
|
|
|
|
|
const editMatch = path.match(/\/agent-jobs\/tasks\/([^\/]+)\/edit$/); |
|
|
if (editMatch) { |
|
|
this.taskId = editMatch[1]; |
|
|
this.isNewTask = false; |
|
|
this.isEditMode = true; |
|
|
this.loadTask(); |
|
|
} else { |
|
|
const match = path.match(/\/agent-jobs\/tasks\/([^\/]+)$/); |
|
|
if (match) { |
|
|
this.taskId = match[1]; |
|
|
this.isNewTask = false; |
|
|
this.isEditMode = false; |
|
|
this.loadTask(); |
|
|
|
|
|
this.fetchJobs(); |
|
|
|
|
|
setInterval(() => { |
|
|
if (!this.isEditMode && !this.isNewTask && this.taskId) { |
|
|
this.fetchJobs(); |
|
|
} |
|
|
}, 2000); |
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
|
|
|
async loadTask() { |
|
|
try { |
|
|
const response = await fetch('/api/agent/tasks/' + this.taskId); |
|
|
if (response.ok) { |
|
|
this.task = await response.json(); |
|
|
|
|
|
|
|
|
let webhooks = []; |
|
|
if (this.task.webhooks && Array.isArray(this.task.webhooks) && this.task.webhooks.length > 0) { |
|
|
|
|
|
webhooks = this.task.webhooks.map(wh => ({ |
|
|
...wh, |
|
|
headers_json: JSON.stringify(wh.headers || {}, null, 2) |
|
|
})); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let multimediaSources = []; |
|
|
if (this.task.multimedia_sources && Array.isArray(this.task.multimedia_sources) && this.task.multimedia_sources.length > 0) { |
|
|
multimediaSources = this.task.multimedia_sources.map(ms => ({ |
|
|
...ms, |
|
|
headers_json: JSON.stringify(ms.headers || {}, null, 2) |
|
|
})); |
|
|
} |
|
|
|
|
|
|
|
|
let cronParamsText = ''; |
|
|
if (this.task.cron_parameters && Object.keys(this.task.cron_parameters).length > 0) { |
|
|
cronParamsText = Object.entries(this.task.cron_parameters) |
|
|
.map(([key, value]) => `${key}=${value}`) |
|
|
.join('\n'); |
|
|
} |
|
|
|
|
|
this.taskForm = { |
|
|
name: this.task.name || '', |
|
|
description: this.task.description || '', |
|
|
model: this.task.model || '', |
|
|
prompt: this.task.prompt || '', |
|
|
enabled: this.task.enabled !== undefined ? this.task.enabled : true, |
|
|
cron: this.task.cron || '', |
|
|
cron_parameters: this.task.cron_parameters || {}, |
|
|
webhooks: webhooks, |
|
|
multimedia_sources: multimediaSources |
|
|
}; |
|
|
this.cronParametersText = cronParamsText; |
|
|
} else { |
|
|
console.error('Failed to load task'); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to load task:', error); |
|
|
} |
|
|
}, |
|
|
|
|
|
async fetchJobs() { |
|
|
if (!this.taskId) return; |
|
|
try { |
|
|
let url = '/api/agent/jobs?task_id=' + this.taskId + '&limit=100'; |
|
|
if (this.jobFilter) { |
|
|
url += '&status=' + this.jobFilter; |
|
|
} |
|
|
const response = await fetch(url); |
|
|
if (response.ok) { |
|
|
this.jobs = await response.json(); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to fetch jobs:', error); |
|
|
} |
|
|
}, |
|
|
|
|
|
enterEditMode() { |
|
|
this.isEditMode = true; |
|
|
}, |
|
|
|
|
|
cancelEdit() { |
|
|
if (this.isNewTask) { |
|
|
window.location.href = '/agent-jobs'; |
|
|
} else { |
|
|
this.isEditMode = false; |
|
|
this.loadTask(); |
|
|
} |
|
|
}, |
|
|
|
|
|
addWebhook() { |
|
|
const webhook = { |
|
|
url: '', |
|
|
method: 'POST', |
|
|
headers: {}, |
|
|
headers_json: '{}', |
|
|
payload_template: '' |
|
|
}; |
|
|
this.taskForm.webhooks.push(webhook); |
|
|
}, |
|
|
|
|
|
addMultimediaSource() { |
|
|
const source = { |
|
|
type: '', |
|
|
url: '', |
|
|
headers: {}, |
|
|
headers_json: '{}' |
|
|
}; |
|
|
if (!this.taskForm.multimedia_sources) { |
|
|
this.taskForm.multimedia_sources = []; |
|
|
} |
|
|
this.taskForm.multimedia_sources.push(source); |
|
|
}, |
|
|
|
|
|
updateCronParameters() { |
|
|
|
|
|
const params = {}; |
|
|
if (this.cronParametersText && this.cronParametersText.trim()) { |
|
|
const lines = this.cronParametersText.trim().split('\n'); |
|
|
for (const line of lines) { |
|
|
const trimmed = line.trim(); |
|
|
if (trimmed) { |
|
|
const equalIndex = trimmed.indexOf('='); |
|
|
if (equalIndex > 0) { |
|
|
const key = trimmed.substring(0, equalIndex).trim(); |
|
|
const value = trimmed.substring(equalIndex + 1).trim(); |
|
|
if (key) { |
|
|
params[key] = value; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
this.taskForm.cron_parameters = params; |
|
|
}, |
|
|
|
|
|
validateCron(cronExpr) { |
|
|
this.cronError = ''; |
|
|
if (!cronExpr || cronExpr.trim() === '') { |
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
const fields = cronExpr.trim().split(/\s+/); |
|
|
if (fields.length !== 5) { |
|
|
this.cronError = 'Cron expression must have exactly 5 fields (minute hour day month weekday)'; |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const validChars = /^[\d\*\s\-\/\,]+$/i; |
|
|
if (!validChars.test(cronExpr) && !cronExpr.match(/[A-Z]{3}/i)) { |
|
|
this.cronError = 'Cron expression contains invalid characters'; |
|
|
return false; |
|
|
} |
|
|
|
|
|
return true; |
|
|
}, |
|
|
|
|
|
async saveTask() { |
|
|
|
|
|
if (this.taskForm.cron && !this.validateCron(this.taskForm.cron)) { |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
this.updateCronParameters(); |
|
|
|
|
|
|
|
|
|
|
|
const taskToSave = { |
|
|
name: this.taskForm.name, |
|
|
description: this.taskForm.description, |
|
|
model: this.taskForm.model, |
|
|
prompt: this.taskForm.prompt, |
|
|
enabled: this.taskForm.enabled, |
|
|
cron: this.taskForm.cron, |
|
|
cron_parameters: this.taskForm.cron_parameters || {}, |
|
|
webhooks: this.taskForm.webhooks.map(webhook => { |
|
|
const headers = {}; |
|
|
try { |
|
|
Object.assign(headers, JSON.parse(webhook.headers_json || '{}')); |
|
|
} catch (e) { |
|
|
console.error('Invalid headers JSON:', e); |
|
|
} |
|
|
return { |
|
|
url: webhook.url, |
|
|
method: webhook.method || 'POST', |
|
|
headers: headers, |
|
|
payload_template: webhook.payload_template || '' |
|
|
}; |
|
|
}) |
|
|
|
|
|
}; |
|
|
|
|
|
try { |
|
|
let response; |
|
|
if (this.isNewTask) { |
|
|
response = await fetch('/api/agent/tasks', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(taskToSave) |
|
|
}); |
|
|
} else { |
|
|
response = await fetch('/api/agent/tasks/' + this.taskId, { |
|
|
method: 'PUT', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(taskToSave) |
|
|
}); |
|
|
} |
|
|
if (response.ok) { |
|
|
if (this.isNewTask) { |
|
|
const result = await response.json(); |
|
|
window.location.href = '/agent-jobs/tasks/' + result.id; |
|
|
} else { |
|
|
this.isEditMode = false; |
|
|
await this.loadTask(); |
|
|
} |
|
|
} else { |
|
|
const error = await response.json(); |
|
|
const errorMsg = error.error || 'Unknown error'; |
|
|
|
|
|
if (errorMsg.toLowerCase().includes('cron')) { |
|
|
this.cronError = errorMsg; |
|
|
} else { |
|
|
alert('Failed to save task: ' + errorMsg); |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to save task:', error); |
|
|
alert('Failed to save task: ' + error.message); |
|
|
} |
|
|
}, |
|
|
|
|
|
showExecuteModal() { |
|
|
this.executionParameters = {}; |
|
|
this.executionParametersText = ''; |
|
|
this.executionMultimedia = {images: '', videos: '', audios: '', files: ''}; |
|
|
this.executeModalTab = 'parameters'; |
|
|
this.showExecuteTaskModal = true; |
|
|
}, |
|
|
|
|
|
parseParameters(text) { |
|
|
const params = {}; |
|
|
if (!text || !text.trim()) { |
|
|
return params; |
|
|
} |
|
|
const lines = text.split('\n'); |
|
|
for (const line of lines) { |
|
|
const trimmed = line.trim(); |
|
|
if (!trimmed) continue; |
|
|
const equalIndex = trimmed.indexOf('='); |
|
|
if (equalIndex > 0) { |
|
|
const key = trimmed.substring(0, equalIndex).trim(); |
|
|
const value = trimmed.substring(equalIndex + 1).trim(); |
|
|
if (key) { |
|
|
params[key] = value; |
|
|
} |
|
|
} |
|
|
} |
|
|
return params; |
|
|
}, |
|
|
|
|
|
async executeTaskWithParameters() { |
|
|
if (!this.task) return; |
|
|
|
|
|
|
|
|
this.executionParameters = this.parseParameters(this.executionParametersText); |
|
|
|
|
|
|
|
|
const parseMultimedia = (text) => { |
|
|
if (!text || !text.trim()) return []; |
|
|
return text.split('\n') |
|
|
.map(line => line.trim()) |
|
|
.filter(line => line.length > 0); |
|
|
}; |
|
|
|
|
|
const requestBody = { |
|
|
task_id: this.task.id, |
|
|
parameters: this.executionParameters, |
|
|
images: parseMultimedia(this.executionMultimedia.images), |
|
|
videos: parseMultimedia(this.executionMultimedia.videos), |
|
|
audios: parseMultimedia(this.executionMultimedia.audios), |
|
|
files: parseMultimedia(this.executionMultimedia.files) |
|
|
}; |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/agent/jobs/execute', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(requestBody) |
|
|
}); |
|
|
if (response.ok) { |
|
|
this.showExecuteTaskModal = false; |
|
|
this.executionParameters = {}; |
|
|
this.executionParametersText = ''; |
|
|
this.executionMultimedia = {images: '', videos: '', audios: '', files: ''}; |
|
|
this.executeModalTab = 'parameters'; |
|
|
this.fetchJobs(); |
|
|
} else { |
|
|
const error = await response.json(); |
|
|
alert('Failed to execute task: ' + (error.error || 'Unknown error')); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to execute task:', error); |
|
|
alert('Failed to execute task: ' + error.message); |
|
|
} |
|
|
}, |
|
|
|
|
|
handleFileUpload(event, type) { |
|
|
const files = event.target.files; |
|
|
if (!files || files.length === 0) return; |
|
|
|
|
|
const dataURIs = []; |
|
|
let processed = 0; |
|
|
|
|
|
for (let i = 0; i < files.length; i++) { |
|
|
const file = files[i]; |
|
|
const reader = new FileReader(); |
|
|
|
|
|
reader.onload = (e) => { |
|
|
const dataURI = e.target.result; |
|
|
dataURIs.push(dataURI); |
|
|
processed++; |
|
|
|
|
|
if (processed === files.length) { |
|
|
|
|
|
const current = this.executionMultimedia[type + 's'] || ''; |
|
|
const newContent = current ? current + '\n' + dataURIs.join('\n') : dataURIs.join('\n'); |
|
|
this.executionMultimedia[type + 's'] = newContent; |
|
|
} |
|
|
}; |
|
|
|
|
|
reader.readAsDataURL(file); |
|
|
} |
|
|
}, |
|
|
|
|
|
async deleteTask() { |
|
|
if (!confirm('Are you sure you want to delete this task? This will also delete all associated jobs.')) return; |
|
|
try { |
|
|
const response = await fetch('/api/agent/tasks/' + this.taskId, { |
|
|
method: 'DELETE' |
|
|
}); |
|
|
if (response.ok) { |
|
|
window.location.href = '/agent-jobs'; |
|
|
} else { |
|
|
const error = await response.json(); |
|
|
alert('Failed to delete task: ' + (error.error || 'Unknown error')); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to delete task:', error); |
|
|
alert('Failed to delete task: ' + error.message); |
|
|
} |
|
|
}, |
|
|
|
|
|
async cancelJob(jobId) { |
|
|
try { |
|
|
const response = await fetch('/api/agent/jobs/' + jobId + '/cancel', { |
|
|
method: 'POST' |
|
|
}); |
|
|
if (response.ok) { |
|
|
this.fetchJobs(); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to cancel job:', error); |
|
|
} |
|
|
}, |
|
|
|
|
|
formatDate(dateStr) { |
|
|
if (!dateStr) return '-'; |
|
|
const date = new Date(dateStr); |
|
|
return date.toLocaleString(); |
|
|
}, |
|
|
|
|
|
async clearJobHistory() { |
|
|
if (!confirm('Are you sure you want to clear all job history for this task? This action cannot be undone.')) return; |
|
|
try { |
|
|
|
|
|
const response = await fetch('/api/agent/jobs?task_id=' + this.taskId + '&limit=1000'); |
|
|
if (response.ok) { |
|
|
const jobs = await response.json(); |
|
|
|
|
|
for (const job of jobs) { |
|
|
await fetch('/api/agent/jobs/' + job.id, { |
|
|
method: 'DELETE' |
|
|
}); |
|
|
} |
|
|
|
|
|
this.fetchJobs(); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to clear job history:', error); |
|
|
alert('Failed to clear job history: ' + error.message); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
</script> |
|
|
</div> |
|
|
</body> |
|
|
</html> |
|
|
|