From cba7d3fefee178c5cd7b34f69d4cdae187cb116a Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Fri, 26 Sep 2025 10:45:45 -0400 Subject: [PATCH] feat: implement custom slash commands with modal-based creation - Add CustomCommandsSettings component with row-based design matching Mode section - Integrate AddCustomCommandModal using proper Dialog components for consistent styling - Remove category filtering and inline editing for cleaner UX - Update modal to use proper Dialog background instead of dark overlay - Remove category field from modal form for simplified command creation - Apply subtle text colors to form helper text for better visual hierarchy - Remove preview section from modal to reduce height and improve usability - Add proper header styling with CardTitle and CardDescription - Update command icons to display without background circles - Implement proper padding and spacing throughout the interface - Connect modal functionality to Plus button and edit actions - Preserve existing command categories while preventing new category assignment --- .../src/components/AddCustomCommandModal.tsx | 114 ++-- .../settings/CustomCommandsSettings.tsx | 490 +++++------------- .../settings/chat/ChatSettingsSection.tsx | 4 + 3 files changed, 174 insertions(+), 434 deletions(-) diff --git a/ui/desktop/src/components/AddCustomCommandModal.tsx b/ui/desktop/src/components/AddCustomCommandModal.tsx index 429bb6c16d..576bb16fd6 100644 --- a/ui/desktop/src/components/AddCustomCommandModal.tsx +++ b/ui/desktop/src/components/AddCustomCommandModal.tsx @@ -1,6 +1,8 @@ import React, { useState, useEffect } from 'react'; -import { X, Plus, Zap, Code, FileText, Search, Play, Settings, Hash } from 'lucide-react'; +import { Plus, Zap, Code, FileText, Search, Play, Settings, Hash } from 'lucide-react'; import { CustomCommand } from '../types/customCommands'; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog'; +import { Button } from './ui/button'; interface AddCustomCommandModalProps { isOpen: boolean; @@ -31,7 +33,6 @@ export const AddCustomCommandModal: React.FC = ({ description: '', prompt: '', icon: 'Zap', - category: '', }); const [errors, setErrors] = useState>({}); @@ -45,7 +46,6 @@ export const AddCustomCommandModal: React.FC = ({ description: editingCommand.description, prompt: editingCommand.prompt, icon: editingCommand.icon || 'Zap', - category: editingCommand.category || '', }); } else { setFormData({ @@ -54,7 +54,6 @@ export const AddCustomCommandModal: React.FC = ({ description: '', prompt: '', icon: 'Zap', - category: '', }); } setErrors({}); @@ -100,7 +99,7 @@ export const AddCustomCommandModal: React.FC = ({ description: formData.description.trim(), prompt: formData.prompt.trim(), icon: formData.icon, - category: formData.category.trim() || undefined, + category: editingCommand?.category || undefined, createdAt: editingCommand?.createdAt || new Date(), updatedAt: new Date(), usageCount: editingCommand?.usageCount || 0, @@ -129,33 +128,23 @@ export const AddCustomCommandModal: React.FC = ({ } }; - if (!isOpen) return null; - return ( -
- {/* Backdrop */} -
- - {/* Modal */} -
- {/* Header */} -
-

+ { + if (!open) { + onClose(); + } + }} + > + + + {editingCommand ? 'Edit Custom Command' : 'Add Custom Command'} -

- -
+ + - {/* Content */} -
+
{/* Command Name */}
@@ -179,7 +168,7 @@ export const AddCustomCommandModal: React.FC = ({ {errors.name && (

{errors.name}

)} -

+

This will be the command users type (e.g., /document)

@@ -201,7 +190,7 @@ export const AddCustomCommandModal: React.FC = ({ {errors.label && (

{errors.label}

)} -

+

Friendly name shown in the command list

@@ -248,22 +237,7 @@ export const AddCustomCommandModal: React.FC = ({
- {/* Category */} -
- - handleInputChange('category', e.target.value)} - placeholder="Documentation, Code, etc." - className="w-full px-3 py-2 border border-borderStandard rounded-md bg-background-default text-textStandard placeholder-textSubtle focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -

- Optional category for organization -

-
+ {/* Prompt */}
@@ -282,52 +256,26 @@ export const AddCustomCommandModal: React.FC = ({ {errors.prompt && (

{errors.prompt}

)} -

+

This is the full prompt that will be sent to the AI when the command is used

- {/* Preview */} -
-

Preview

-
-
User types:
-
- /{formData.name || 'command'} -
-
Appears as pill:
-
- {iconOptions.find(opt => opt.name === formData.icon)?.icon} - {formData.label || 'Command Label'} -
-
AI receives:
-
- {formData.prompt || 'Your prompt will appear here...'} -
-
-
+
- {/* Footer */} -
- - + -
-
- + + + + ); }; diff --git a/ui/desktop/src/components/settings/CustomCommandsSettings.tsx b/ui/desktop/src/components/settings/CustomCommandsSettings.tsx index ddc64b0f5b..415bffa28d 100644 --- a/ui/desktop/src/components/settings/CustomCommandsSettings.tsx +++ b/ui/desktop/src/components/settings/CustomCommandsSettings.tsx @@ -2,14 +2,11 @@ import React, { useState, useEffect } from 'react'; import { Plus, Edit, Trash2, Copy, Download, Upload, Star, StarOff, Zap, Code, FileText, Search } from 'lucide-react'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; -import { Textarea } from '../ui/textarea'; -import { Label } from '../ui/label'; +import { AddCustomCommandModal } from '../AddCustomCommandModal'; import { CustomCommand, CustomCommandCategory, - COMMAND_TEMPLATE, - DEFAULT_CATEGORIES, - COMMAND_VALIDATION + DEFAULT_CATEGORIES } from '../../types/customCommands'; interface CustomCommandsSettingsProps { @@ -26,12 +23,9 @@ const ICON_MAP = { export const CustomCommandsSettings: React.FC = () => { const [commands, setCommands] = useState([]); const [categories] = useState(DEFAULT_CATEGORIES); - const [isEditing, setIsEditing] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); const [editingCommand, setEditingCommand] = useState(null); - const [formData, setFormData] = useState(COMMAND_TEMPLATE); const [searchQuery, setSearchQuery] = useState(''); - const [selectedCategory, setSelectedCategory] = useState('all'); - const [errors, setErrors] = useState>({}); // Load commands from storage on mount useEffect(() => { @@ -67,48 +61,7 @@ export const CustomCommandsSettings: React.FC = () } }; - const validateForm = (): boolean => { - const newErrors: Record = {}; - - // Validate name - if (!formData.name) { - newErrors.name = 'Command name is required'; - } else if (formData.name.length < COMMAND_VALIDATION.name.minLength) { - newErrors.name = `Name must be at least ${COMMAND_VALIDATION.name.minLength} characters`; - } else if (!COMMAND_VALIDATION.name.pattern.test(formData.name)) { - newErrors.name = 'Name must start with a letter and contain only letters and numbers'; - } else if (commands.some(cmd => cmd.name.toLowerCase() === formData.name.toLowerCase() && cmd.id !== editingCommand?.id)) { - newErrors.name = 'A command with this name already exists'; - } - - // Validate label - if (!formData.label) { - newErrors.label = 'Label is required'; - } else if (formData.label.length < COMMAND_VALIDATION.label.minLength) { - newErrors.label = `Label must be at least ${COMMAND_VALIDATION.label.minLength} characters`; - } - - // Validate description - if (!formData.description) { - newErrors.description = 'Description is required'; - } else if (formData.description.length < COMMAND_VALIDATION.description.minLength) { - newErrors.description = `Description must be at least ${COMMAND_VALIDATION.description.minLength} characters`; - } - - // Validate prompt - if (!formData.prompt) { - newErrors.prompt = 'Prompt is required'; - } else if (formData.prompt.length < COMMAND_VALIDATION.prompt.minLength) { - newErrors.prompt = `Prompt must be at least ${COMMAND_VALIDATION.prompt.minLength} characters`; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSave = () => { - if (!validateForm()) return; - + const handleModalSave = (command: CustomCommand) => { const now = new Date(); let updatedCommands: CustomCommand[]; @@ -116,42 +69,30 @@ export const CustomCommandsSettings: React.FC = () // Update existing command updatedCommands = commands.map(cmd => cmd.id === editingCommand.id - ? { ...formData, id: editingCommand.id, createdAt: editingCommand.createdAt, updatedAt: now } + ? { ...command, id: editingCommand.id, createdAt: editingCommand.createdAt, updatedAt: now } : cmd ); } else { // Create new command - const newCommand: CustomCommand = { - ...formData, - id: `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - createdAt: now, - updatedAt: now, - }; - updatedCommands = [...commands, newCommand]; + updatedCommands = [...commands, { ...command, createdAt: now, updatedAt: now }]; } saveCommands(updatedCommands); - setIsEditing(false); - setEditingCommand(null); - setFormData(COMMAND_TEMPLATE); - setErrors({}); }; const handleEdit = (command: CustomCommand) => { setEditingCommand(command); - setFormData({ - name: command.name, - label: command.label, - description: command.description, - prompt: command.prompt, - icon: command.icon || 'Zap', - category: command.category || 'general', - variables: command.variables || [], - usageCount: command.usageCount, - isFavorite: command.isFavorite, - }); - setIsEditing(true); - setErrors({}); + setIsModalOpen(true); + }; + + const handleCreateNew = () => { + setEditingCommand(null); + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + setEditingCommand(null); }; const handleDelete = (commandId: string) => { @@ -187,286 +128,133 @@ export const CustomCommandsSettings: React.FC = () cmd.label.toLowerCase().includes(searchQuery.toLowerCase()) || cmd.description.toLowerCase().includes(searchQuery.toLowerCase()); - const matchesCategory = selectedCategory === 'all' || cmd.category === selectedCategory; - - return matchesSearch && matchesCategory; + return matchesSearch; }); - const handleCancel = () => { - setIsEditing(false); - setEditingCommand(null); - setFormData(COMMAND_TEMPLATE); - setErrors({}); - }; - return ( -
- {!isEditing ? ( - <> - {/* Header Actions */} -
-
- setSearchQuery(e.target.value)} - className="w-64" - /> - -
-
- - - -
-
- - {/* Commands List - Row Style */} - {filteredCommands.map(command => ( -
-
-
-
- {ICON_MAP[command.icon as keyof typeof ICON_MAP] || ICON_MAP.Zap} -
-
-

/{command.name}

-

{command.description}

-
-
- -
- - - - -
-
-
- ))} - - {filteredCommands.length === 0 && ( -
-
- -
-

- {searchQuery || selectedCategory !== 'all' ? 'No commands found' : 'No custom commands yet'} -

-

- {searchQuery || selectedCategory !== 'all' - ? 'Try adjusting your search or category filter' - : 'Create your first custom slash command to get started' - } -

- {!searchQuery && selectedCategory === 'all' && ( - - )} -
- )} - - ) : ( - /* Command Editor */ -
-
-

- {editingCommand ? 'Edit Command' : 'Create New Command'} -

-
- - -
-
- -
-
-
- - setFormData({ ...formData, name: e.target.value })} - className={errors.name ? 'border-red-500' : ''} - /> - {errors.name &&

{errors.name}

} -

- Users will type /{formData.name || 'name'} to use this command -

-
- -
- - setFormData({ ...formData, label: e.target.value })} - className={errors.label ? 'border-red-500' : ''} - /> - {errors.label &&

{errors.label}

} -
-
- -
- - setFormData({ ...formData, description: e.target.value })} - className={errors.description ? 'border-red-500' : ''} - /> - {errors.description &&

{errors.description}

} -
- -
-
- - -
- -
- - -
-
- -
- -