| <!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> |
|
|