From 3fcfd42b9a1c6e3e95d3cf8aec208efdef7eac75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sat, 20 Dec 2025 21:20:13 +0100 Subject: [PATCH] feat: simplify SpellbookDropdown to only manage layouts --- src/components/SpellbookDropdown.tsx | 165 +++++++-------------------- 1 file changed, 43 insertions(+), 122 deletions(-) diff --git a/src/components/SpellbookDropdown.tsx b/src/components/SpellbookDropdown.tsx index bb7eb2a..5d6f966 100644 --- a/src/components/SpellbookDropdown.tsx +++ b/src/components/SpellbookDropdown.tsx @@ -1,18 +1,12 @@ import { useMemo, useState } from "react"; -import { BookHeart, ChevronDown, Plus, Save, WandSparkles, X } from "lucide-react"; +import { BookHeart, ChevronDown, Plus, Save, X } from "lucide-react"; import { useLiveQuery } from "dexie-react-hooks"; import db from "@/services/db"; import { useGrimoire } from "@/core/state"; import { useReqTimeline } from "@/hooks/useReqTimeline"; import { createSpellbook, parseSpellbook } from "@/lib/spellbook-manager"; -import { decodeSpell } from "@/lib/spell-conversion"; -import type { - SpellbookEvent, - ParsedSpellbook, - SpellEvent, - ParsedSpell, -} from "@/types/spell"; -import { SPELLBOOK_KIND, SPELL_KIND } from "@/constants/kinds"; +import type { SpellbookEvent, ParsedSpellbook } from "@/types/spell"; +import { SPELLBOOK_KIND } from "@/constants/kinds"; import { Button } from "./ui/button"; import { DropdownMenu, @@ -23,14 +17,14 @@ import { DropdownMenuTrigger, } from "./ui/dropdown-menu"; import { toast } from "sonner"; -import { manPages } from "@/types/man"; import { cn } from "@/lib/utils"; import { PublishSpellbookAction } from "@/actions/publish-spellbook"; import { saveSpellbook } from "@/services/spellbook-storage"; import { SaveSpellbookDialog } from "./SaveSpellbookDialog"; export function SpellbookDropdown() { - const { state, loadSpellbook, addWindow, clearActiveSpellbook } = useGrimoire(); + const { state, loadSpellbook, addWindow, clearActiveSpellbook } = + useGrimoire(); const activeAccount = state.activeAccount; const activeSpellbook = state.activeSpellbook; const [saveDialogOpen, setSaveDialogOpen] = useState(false); @@ -40,15 +34,12 @@ export function SpellbookDropdown() { const localSpellbooks = useLiveQuery(() => db.spellbooks.toArray().then((books) => books.filter((b) => !b.deletedAt)), ); - const localSpells = useLiveQuery(() => - db.spells.toArray().then((spells) => spells.filter((s) => !s.deletedAt)), - ); // 2. Fetch Network Data - const { events: networkEvents, loading: networkLoading } = useReqTimeline( - activeAccount ? `header-resources-${activeAccount.pubkey}` : "none", + const { events: networkEvents } = useReqTimeline( + activeAccount ? `header-spellbooks-${activeAccount.pubkey}` : "none", activeAccount - ? { kinds: [SPELLBOOK_KIND, SPELL_KIND], authors: [activeAccount.pubkey] } + ? { kinds: [SPELLBOOK_KIND], authors: [activeAccount.pubkey] } : [], activeAccount?.relays?.map((r) => r.url) || [], { stream: true }, @@ -70,7 +61,7 @@ export function SpellbookDropdown() { }); } - for (const event of networkEvents.filter((e) => e.kind === SPELLBOOK_KIND)) { + for (const event of networkEvents) { const slug = event.tags.find((t) => t[0] === "d")?.[1] || ""; if (!slug) continue; const existing = allMap.get(slug); @@ -91,43 +82,7 @@ export function SpellbookDropdown() { ); }, [localSpellbooks, networkEvents, activeAccount]); - // 4. Process Spells - const spells = useMemo(() => { - if (!activeAccount) return []; - const allMap = new Map(); - - for (const s of localSpells || []) { - // Use eventId if available, otherwise fallback to local id for deduplication - const key = s.eventId || s.id; - allMap.set(key, { - name: s.name || s.alias, - command: s.command, - description: s.description, - event: s.event as SpellEvent, - filter: {}, - topics: [], - closeOnEose: false, - }); - } - - for (const event of networkEvents.filter((e) => e.kind === SPELL_KIND)) { - if (allMap.has(event.id)) continue; - try { - allMap.set(event.id, decodeSpell(event as SpellEvent)); - } catch (e) { - // ignore - } - } - - return Array.from(allMap.values()).sort((a, b) => - (a.name || "Untitled").localeCompare(b.name || "Untitled"), - ); - }, [localSpells, networkEvents, activeAccount]); - - if ( - !activeAccount || - (spellbooks.length === 0 && spells.length === 0 && !networkLoading) - ) { + if (!activeAccount || (spellbooks.length === 0 && !activeSpellbook)) { return null; } @@ -145,11 +100,14 @@ export function SpellbookDropdown() { state, title: activeSpellbook.title, }); - + const content = JSON.parse(encoded.eventProps.content); // 1. Save locally - const local = await db.spellbooks.where("slug").equals(activeSpellbook.slug).first(); + const local = await db.spellbooks + .where("slug") + .equals(activeSpellbook.slug) + .first(); if (local) { await db.spellbooks.update(local.id, { content }); } else { @@ -170,7 +128,9 @@ export function SpellbookDropdown() { content, localId: local?.id, }); - toast.success(`Layout "${activeSpellbook.title}" updated and published`); + toast.success( + `Layout "${activeSpellbook.title}" updated and published`, + ); } else { toast.success(`Layout "${activeSpellbook.title}" updated locally`); } @@ -181,31 +141,15 @@ export function SpellbookDropdown() { } }; - const handleRunSpell = async (spell: ParsedSpell) => { - try { - const parts = spell.command.trim().split(/\s+/); - const commandName = parts[0]?.toLowerCase(); - const cmdArgs = parts.slice(1); - const command = manPages[commandName]; - - if (command) { - const cmdProps = command.argParser - ? await Promise.resolve(command.argParser(cmdArgs)) - : command.defaultProps || {}; - addWindow(command.appId, cmdProps, spell.command); - toast.success(`Ran spell: ${spell.name || "Untitled"}`); - } - } catch (e) { - toast.error("Failed to run spell"); - } - }; - const itemClass = "cursor-pointer py-2 hover:bg-muted focus:bg-muted transition-colors"; return ( <> - + @@ -240,8 +184,12 @@ export function SpellbookDropdown() { >
- Update "{activeSpellbook.title}" - Save current state to this spellbook + + Update "{activeSpellbook.title}" + + + Save current state to this spellbook +
0 && ( <> - Spellbooks + My Layouts {spellbooks.map((sb) => { const isActive = activeSpellbook?.slug === sb.slug; @@ -269,7 +217,12 @@ export function SpellbookDropdown() { onClick={() => handleApplySpellbook(sb)} className={cn(itemClass, isActive && "bg-muted font-bold")} > - +
{sb.title} @@ -286,56 +239,24 @@ export function SpellbookDropdown() { className={cn(itemClass, "text-xs opacity-70")} > - Manage Library + Manage Layouts )} - {/* New/Save Section */} + {/* New Section */} setSaveDialogOpen(true)} className={itemClass} > - Save as new layout + + Save as new layout + - - {/* Spells Section */} - {spells.length > 0 && ( - <> - - - Spells - - {spells.map((s, idx) => ( - handleRunSpell(s)} - className={itemClass} - > - -
- - {s.name || "Untitled Spell"} - - - {s.command} - -
-
- ))} - addWindow("spells", {})} - className={cn(itemClass, "text-xs opacity-70")} - > - - Manage Spells - - - )} ); -} +} \ No newline at end of file