- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
301 lines
12 KiB
TypeScript
301 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useCreatePrompt } from './hooks/usePrompts';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
} from './ui/dialog';
|
|
import { Label } from './ui/label';
|
|
import { Input } from './ui/input';
|
|
import { Textarea } from './ui/textarea';
|
|
import { Button } from './ui/button';
|
|
import { Badge } from './ui/badge';
|
|
import { Alert, AlertDescription } from './ui/alert';
|
|
import { Loader2, Trash2, Upload, FileText } from 'lucide-react';
|
|
|
|
interface CreatePromptModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onCreated: (prompt: { name: string }) => void;
|
|
}
|
|
|
|
interface ResourcePayload {
|
|
data: string;
|
|
mimeType: string;
|
|
filename?: string;
|
|
}
|
|
|
|
export default function CreatePromptModal({ open, onClose, onCreated }: CreatePromptModalProps) {
|
|
const [name, setName] = useState('');
|
|
const [title, setTitle] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [content, setContent] = useState('');
|
|
const [resource, setResource] = useState<ResourcePayload | null>(null);
|
|
const [resourcePreview, setResourcePreview] = useState<string | null>(null);
|
|
const [resourceName, setResourceName] = useState<string | null>(null);
|
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
|
|
const createPromptMutation = useCreatePrompt();
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setName('');
|
|
setTitle('');
|
|
setDescription('');
|
|
setContent('');
|
|
setResource(null);
|
|
setResourcePreview(null);
|
|
setResourceName(null);
|
|
setIsDragOver(false);
|
|
createPromptMutation.reset();
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [open]);
|
|
|
|
const handleFile = async (file: File) => {
|
|
try {
|
|
const data = await readFileAsDataUrl(file);
|
|
setResource({
|
|
data,
|
|
mimeType: file.type || 'application/octet-stream',
|
|
filename: file.name,
|
|
});
|
|
setResourcePreview(data);
|
|
setResourceName(file.name);
|
|
} catch (error) {
|
|
console.error('Failed to read file:', error);
|
|
}
|
|
};
|
|
|
|
const handleResourceChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) {
|
|
setResource(null);
|
|
setResourcePreview(null);
|
|
setResourceName(null);
|
|
return;
|
|
}
|
|
await handleFile(file);
|
|
};
|
|
|
|
const handleDragOver = (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
setIsDragOver(true);
|
|
};
|
|
|
|
const handleDragLeave = (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
setIsDragOver(false);
|
|
};
|
|
|
|
const handleDrop = async (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
setIsDragOver(false);
|
|
|
|
const files = Array.from(e.dataTransfer.files);
|
|
if (files.length > 0) {
|
|
await handleFile(files[0]);
|
|
}
|
|
};
|
|
|
|
const removeResource = () => {
|
|
setResource(null);
|
|
setResourcePreview(null);
|
|
setResourceName(null);
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
if (!name.trim() || !content.trim()) {
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
name: name.trim(),
|
|
title: title.trim() || undefined,
|
|
description: description.trim() || undefined,
|
|
content,
|
|
resource: resource || undefined,
|
|
};
|
|
|
|
createPromptMutation.mutate(payload, {
|
|
onSuccess: (data) => {
|
|
if (data?.prompt) {
|
|
onCreated({ name: data.prompt.name });
|
|
}
|
|
onClose();
|
|
},
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
|
<DialogContent className="sm:max-w-3xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Create Custom Prompt</DialogTitle>
|
|
<DialogDescription>
|
|
Define reusable prompt text and optionally attach a supporting resource
|
|
file.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
{createPromptMutation.error && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>
|
|
{createPromptMutation.error instanceof Error
|
|
? createPromptMutation.error.message
|
|
: 'Failed to create prompt'}
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="prompt-name">Prompt Name</Label>
|
|
<Input
|
|
id="prompt-name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="research-summary"
|
|
required
|
|
/>
|
|
<p className="text-[11px] text-muted-foreground">
|
|
Use lowercase letters, numbers, or hyphens. This becomes your
|
|
/prompt command.
|
|
</p>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="prompt-title">Title</Label>
|
|
<Input
|
|
id="prompt-title"
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
placeholder="Research Summary"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="prompt-description">Description</Label>
|
|
<Input
|
|
id="prompt-description"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="Summarize research papers with key findings"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="prompt-content">Prompt Content</Label>
|
|
<Textarea
|
|
id="prompt-content"
|
|
value={content}
|
|
onChange={(e) => setContent(e.target.value)}
|
|
placeholder="Write the instructions for this prompt..."
|
|
className="min-h-[160px]"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label>Attach resource (optional)</Label>
|
|
{resourcePreview ? (
|
|
<div className="flex items-center justify-between rounded-lg border border-dashed border-border/60 bg-muted/40 px-4 py-3">
|
|
<div className="flex items-center text-sm">
|
|
<FileText className="h-4 w-4 mr-2 text-muted-foreground" />
|
|
<Badge variant="secondary" className="mr-2">
|
|
Resource
|
|
</Badge>
|
|
{resourceName || 'Attached file'}
|
|
</div>
|
|
<Button variant="ghost" size="sm" onClick={removeResource}>
|
|
<Trash2 className="h-4 w-4 mr-1" /> Remove
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div
|
|
className={`relative rounded-lg border-2 border-dashed transition-colors ${
|
|
isDragOver
|
|
? 'border-primary bg-primary/5'
|
|
: 'border-border/60 hover:border-border/80'
|
|
} px-6 py-8 text-center`}
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
>
|
|
<input
|
|
type="file"
|
|
onChange={handleResourceChange}
|
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
accept="*/*"
|
|
/>
|
|
<div className="flex flex-col items-center justify-center space-y-2">
|
|
<Upload
|
|
className={`h-8 w-8 ${isDragOver ? 'text-primary' : 'text-muted-foreground'}`}
|
|
/>
|
|
<div className="text-sm">
|
|
<span className="font-medium">
|
|
{isDragOver ? 'Drop file here' : 'Click to upload'}
|
|
</span>
|
|
<span className="text-muted-foreground">
|
|
{' '}
|
|
or drag and drop
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Any file type supported
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<p className="text-[11px] text-muted-foreground">
|
|
The resource will be stored securely and referenced when this prompt is
|
|
used.
|
|
</p>
|
|
</div>
|
|
|
|
<DialogFooter className="flex items-center justify-between gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={onClose}
|
|
disabled={createPromptMutation.isPending}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={createPromptMutation.isPending}>
|
|
{createPromptMutation.isPending ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> Saving...
|
|
</>
|
|
) : (
|
|
'Save Prompt'
|
|
)}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function readFileAsDataUrl(file: File): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
const result = reader.result;
|
|
if (typeof result === 'string') {
|
|
resolve(result);
|
|
} else {
|
|
reject(new Error('Failed to read file'));
|
|
}
|
|
};
|
|
reader.onerror = (error) => reject(error);
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|