diff --git a/client/src/components/QuickReplies.jsx b/client/src/components/QuickReplies.jsx index 204aea76..eda760ab 100644 --- a/client/src/components/QuickReplies.jsx +++ b/client/src/components/QuickReplies.jsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from "react"; import { Plus, X, Check, Pencil, GripVertical, ChevronLeft, ChevronRight } from "lucide-react"; -const CATEGORIES = ["General", "Social", "Needs", "Urgent"]; +const DEFAULT_CATEGORIES = ["General", "Social", "Needs", "Urgent"]; +const CATEGORIES_KEY = "vf_quick_reply_categories"; const generateId = () => Math.random().toString(36).substr(2, 9); @@ -18,6 +19,20 @@ const DEFAULT_QUICK_REPLIES = [ const STORAGE_KEY = "vf_quick_replies"; export function QuickReplies({ onSelect, showToast }) { + const [categories, setCategories] = useState(() => { + try { + const saved = localStorage.getItem(CATEGORIES_KEY); + if (saved === null) return DEFAULT_CATEGORIES; + const parsed = JSON.parse(saved); + if (Array.isArray(parsed) && parsed.every((c) => typeof c === "string")) { + return parsed; + } + return DEFAULT_CATEGORIES; + } catch { + return DEFAULT_CATEGORIES; + } + }); + const [replies, setReplies] = useState(() => { try { const saved = localStorage.getItem(STORAGE_KEY); @@ -27,11 +42,19 @@ export function QuickReplies({ onSelect, showToast }) { Array.isArray(parsed) && parsed.every((item) => item && typeof item.phrase === "string" && typeof item.label === "string") ) { - return parsed.map((item, idx) => ({ + // Load initial categories to validate + let cats = DEFAULT_CATEGORIES; + try { + const savedCats = localStorage.getItem(CATEGORIES_KEY); + if (savedCats) { + const parsedCats = JSON.parse(savedCats); + if (Array.isArray(parsedCats)) cats = parsedCats; + } + } catch {} + return parsed.map((item) => ({ ...item, id: item.id || generateId(), - hotkey: item.hotkey || String(idx + 1), - category: item.category && CATEGORIES.includes(item.category) ? item.category : "General", + category: item.category && cats.includes(item.category) ? item.category : "General", })); } return DEFAULT_QUICK_REPLIES; @@ -47,7 +70,9 @@ export function QuickReplies({ onSelect, showToast }) { const [newPhrase, setNewPhrase] = useState(""); const [selectedCategoryTab, setSelectedCategoryTab] = useState("All"); const [newCategory, setNewCategory] = useState("General"); - const [draggedId, setDraggedId] = useState(null); + const [isAddingCategory, setIsAddingCategory] = useState(false); + const [newCategoryInput, setNewCategoryInput] = useState(""); + const tablistRef = React.useRef(null); useEffect(() => { try { @@ -57,45 +82,107 @@ export function QuickReplies({ onSelect, showToast }) { } }, [replies]); - const handleDragStart = (e, id) => { - setDraggedId(id); - e.dataTransfer.effectAllowed = "move"; - }; + useEffect(() => { + try { + localStorage.setItem(CATEGORIES_KEY, JSON.stringify(categories)); + } catch { + console.error('Failed to persist categories to localStorage'); + } + }, [categories]); - const handleDragOver = (e) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - }; + useEffect(() => { + function handleSync() { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + const parsed = JSON.parse(saved); + if (Array.isArray(parsed)) { + setReplies(parsed); + } + } + } catch (err) { + console.error("Failed to sync quick replies:", err); + } + + try { + const savedCategories = localStorage.getItem(CATEGORIES_KEY); + if (savedCategories) { + const parsed = JSON.parse(savedCategories); + if (Array.isArray(parsed)) { + setCategories(parsed); + } + } + } catch (err) { + console.error("Failed to sync categories:", err); + } + } + + window.addEventListener("storage", handleSync); + window.addEventListener("voiceforge:quickRepliesChanged", handleSync); + window.addEventListener("voiceforge:quickReplyCategoriesChanged", handleSync); - const handleDrop = (e, targetId) => { + return () => { + window.removeEventListener("storage", handleSync); + window.removeEventListener("voiceforge:quickRepliesChanged", handleSync); + window.removeEventListener("voiceforge:quickReplyCategoriesChanged", handleSync); + }; + }, []); + + const handleAddCategory = (e) => { e.preventDefault(); - if (!draggedId || draggedId === targetId) return; - - setReplies((prev) => { - const fromIndex = prev.findIndex((r) => r.id === draggedId); - const toIndex = prev.findIndex((r) => r.id === targetId); - if (fromIndex === -1 || toIndex === -1) return prev; - - const updated = [...prev]; - const [movedItem] = updated.splice(fromIndex, 1); - updated.splice(toIndex, 0, movedItem); - return updated; - }); - setDraggedId(null); + const cleanCat = newCategoryInput.trim(); + + if (!cleanCat) { + showToast("Category name cannot be empty", "error"); + return; + } + + if (cleanCat.length > 20) { + showToast("Category name is too long (max 20 characters)", "error"); + return; + } + + const isDuplicate = categories.some( + (c) => c.toLowerCase() === cleanCat.toLowerCase() + ); + + if (isDuplicate || cleanCat.toLowerCase() === "all") { + showToast("Category already exists", "error"); + return; + } + + const nextCats = [...categories, cleanCat]; + setCategories(nextCats); + setNewCategoryInput(""); + setIsAddingCategory(false); + showToast("Category added", "success"); + window.dispatchEvent(new Event("voiceforge:quickReplyCategoriesChanged")); }; - const handleMove = (id, direction) => { - setReplies((prev) => { - const index = prev.findIndex((r) => r.id === id); - if (index === -1) return prev; - const newIndex = direction === "left" ? index - 1 : index + 1; - if (newIndex < 0 || newIndex >= prev.length) return prev; - - const updated = [...prev]; - const [movedItem] = updated.splice(index, 1); - updated.splice(newIndex, 0, movedItem); - return updated; - }); + const handleDeleteCategory = (catToDelete) => { + const confirmDelete = window.confirm( + `Are you sure you want to delete the category "${catToDelete}"? Existing replies in this category will be moved to "General".` + ); + if (!confirmDelete) return; + + const nextCats = categories.filter((c) => c !== catToDelete); + setCategories(nextCats); + + setReplies((prev) => + prev.map((r) => { + if (r.category === catToDelete) { + return { ...r, category: "General" }; + } + return r; + }) + ); + + if (selectedCategoryTab === catToDelete) { + setSelectedCategoryTab("All"); + } + + showToast("Category deleted and phrases moved to General", "success"); + window.dispatchEvent(new Event("voiceforge:quickReplyCategoriesChanged")); }; const handleAdd = (e) => { @@ -250,30 +337,89 @@ export function QuickReplies({ onSelect, showToast }) { {/* Category Tabs */}