Files
SuperCharged-Claude-Code-Up…/dexto/packages/webui/components/CreatePromptModal.tsx
admin b52318eeae feat: Add intelligent auto-router and enhanced integrations
- 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>
2026-01-28 00:27:56 +04:00

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);
});
}