From 7ae92da16e426e8bd97121c31cbed35d68b49d89 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 20 Feb 2026 00:19:17 -0800 Subject: [PATCH] feat(local-llm): Phase B quick actions + command palette (B1-B6) - add fuse.js dependency and command palette modal (Cmd+K) - add built-in quick actions library (30 templates across categories) - add quick action CRUD + seeding + import/export helpers in db layer - seed quick actions on workspace load and list top actions in sidebar - implement quick action launcher -> creates preconfigured conversation - add custom quick action editor modal for creating/editing actions - wire command palette system actions and conversation navigation - support passing QA template into conversation input via query param --- __LOCAL_LLMs/dashboard/package.json | 1 + .../src/app/(workspace)/c/[id]/page.tsx | 10 +- .../(workspace)/components/CommandPalette.tsx | 169 +++++++++ .../components/ConversationView.tsx | 3 + .../components/QuickActionEditor.tsx | 205 +++++++++++ .../app/(workspace)/components/Sidebar.tsx | 28 +- .../dashboard/src/app/(workspace)/layout.tsx | 92 ++++- __LOCAL_LLMs/dashboard/src/app/lib/db.ts | 81 +++++ .../dashboard/src/app/lib/quick-actions.ts | 337 ++++++++++++++++++ pnpm-lock.yaml | 12 + 10 files changed, 934 insertions(+), 4 deletions(-) create mode 100644 __LOCAL_LLMs/dashboard/src/app/(workspace)/components/CommandPalette.tsx create mode 100644 __LOCAL_LLMs/dashboard/src/app/(workspace)/components/QuickActionEditor.tsx create mode 100644 __LOCAL_LLMs/dashboard/src/app/lib/quick-actions.ts diff --git a/__LOCAL_LLMs/dashboard/package.json b/__LOCAL_LLMs/dashboard/package.json index 0ae97f2a..eb9c30d5 100644 --- a/__LOCAL_LLMs/dashboard/package.json +++ b/__LOCAL_LLMs/dashboard/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@types/react-syntax-highlighter": "^15.5.13", + "fuse.js": "^7.1.0", "idb": "^8.0.3", "lucide-react": "^0.575.0", "next": "16.1.6", diff --git a/__LOCAL_LLMs/dashboard/src/app/(workspace)/c/[id]/page.tsx b/__LOCAL_LLMs/dashboard/src/app/(workspace)/c/[id]/page.tsx index b6ae1314..d0681b59 100644 --- a/__LOCAL_LLMs/dashboard/src/app/(workspace)/c/[id]/page.tsx +++ b/__LOCAL_LLMs/dashboard/src/app/(workspace)/c/[id]/page.tsx @@ -2,11 +2,13 @@ import Link from 'next/link'; import { useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; import { getConversation, getMessages } from '../../../lib/db'; import type { Conversation, Message } from '../../../lib/types'; import { ConversationView } from '../../components/ConversationView'; export default function ConversationPage({ params }: { params: { id: string } }) { + const searchParams = useSearchParams(); const [conversation, setConversation] = useState(null); const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); @@ -47,5 +49,11 @@ export default function ConversationPage({ params }: { params: { id: string } }) ); } - return ; + return ( + + ); } diff --git a/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/CommandPalette.tsx b/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/CommandPalette.tsx new file mode 100644 index 00000000..dbbf0bc5 --- /dev/null +++ b/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/CommandPalette.tsx @@ -0,0 +1,169 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import Fuse from 'fuse.js'; +import type { Conversation, QuickAction } from '../../lib/types'; + +type CommandItem = + | { + type: 'qa'; + id: string; + name: string; + description: string; + icon: string; + payload: QuickAction; + } + | { + type: 'conversation'; + id: string; + name: string; + description: string; + icon: string; + payload: Conversation; + } + | { + type: 'system'; + id: string; + name: string; + description: string; + icon: string; + action: 'mission-control' | 'settings' | 'export'; + }; + +interface CommandPaletteProps { + open: boolean; + quickActions: QuickAction[]; + conversations: Conversation[]; + onClose: () => void; + onSelectQuickAction: (qa: QuickAction) => void; + onSelectConversation: (conversation: Conversation) => void; + onSelectSystem: (action: 'mission-control' | 'settings' | 'export') => void; +} + +export function CommandPalette({ + open, + quickActions, + conversations, + onClose, + onSelectQuickAction, + onSelectConversation, + onSelectSystem, +}: CommandPaletteProps) { + const [query, setQuery] = useState(''); + + const items = useMemo(() => { + const qaItems: CommandItem[] = quickActions.map(qa => ({ + type: 'qa', + id: qa.id, + name: qa.name, + description: qa.description, + icon: qa.icon, + payload: qa, + })); + + const convItems: CommandItem[] = conversations.slice(0, 20).map(conv => ({ + type: 'conversation', + id: conv.id, + name: conv.title, + description: conv.model || 'Conversation', + icon: '๐Ÿ’ฌ', + payload: conv, + })); + + const systemItems: CommandItem[] = [ + { + type: 'system', + id: 'sys-mission-control', + name: 'Mission Control', + description: 'Open model and system dashboard', + icon: '๐ŸŽ›๏ธ', + action: 'mission-control', + }, + { + type: 'system', + id: 'sys-settings', + name: 'Settings', + description: 'Open settings panel', + icon: 'โš™๏ธ', + action: 'settings', + }, + { + type: 'system', + id: 'sys-export', + name: 'Export Data', + description: 'Export workspace data', + icon: '๐Ÿ“ฆ', + action: 'export', + }, + ]; + + return [...qaItems, ...convItems, ...systemItems]; + }, [quickActions, conversations]); + + const results = useMemo(() => { + if (!query.trim()) return items.slice(0, 15); + const fuse = new Fuse(items, { + keys: ['name', 'description'], + threshold: 0.35, + ignoreLocation: true, + includeScore: true, + }); + return fuse + .search(query) + .map(r => r.item) + .slice(0, 15); + }, [items, query]); + + if (!open) return null; + + return ( +
+
e.stopPropagation()} + > + setQuery(e.target.value)} + placeholder="Search actions, conversations, commands..." + className="mb-3 w-full rounded-md border border-white/10 bg-[var(--surface-muted)] px-3 py-2 text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-tertiary)]" + onKeyDown={e => { + if (e.key === 'Escape') onClose(); + }} + /> + +
+ {results.map(item => ( + + ))} + {results.length === 0 && ( +

No results

+ )} +
+
+
+ ); +} diff --git a/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/ConversationView.tsx b/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/ConversationView.tsx index 6e74ed9d..eb05882d 100644 --- a/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/ConversationView.tsx +++ b/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/ConversationView.tsx @@ -12,12 +12,14 @@ import { estimateTokens, getModelContextWindow } from '../../lib/format'; interface ConversationViewProps { conversation: Conversation; initialMessages: Message[]; + initialTemplate?: string; onConversationUpdated?: (next: Conversation) => void; } export function ConversationView({ conversation, initialMessages, + initialTemplate, onConversationUpdated, }: ConversationViewProps) { const [title, setTitle] = useState(conversation.title); @@ -269,6 +271,7 @@ export function ConversationView({ streaming={streaming} onCancel={onCancel} disabled={!selectedModel} + initialText={initialTemplate || ''} /> ); diff --git a/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/QuickActionEditor.tsx b/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/QuickActionEditor.tsx new file mode 100644 index 00000000..caf11250 --- /dev/null +++ b/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/QuickActionEditor.tsx @@ -0,0 +1,205 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import type { QuickAction } from '../../lib/types'; + +interface QuickActionEditorProps { + open: boolean; + editing?: QuickAction | null; + onClose: () => void; + onSave: (action: QuickAction) => Promise; +} + +const CATEGORIES: QuickAction['category'][] = [ + 'code', + 'writing', + 'analysis', + 'creative', + 'devops', + 'custom', +]; + +const MODEL_HINTS = ['fast', 'coding', 'reasoning', 'chat', 'vision']; + +export function QuickActionEditor({ open, editing, onClose, onSave }: QuickActionEditorProps) { + const [name, setName] = useState(''); + const [icon, setIcon] = useState('โœจ'); + const [category, setCategory] = useState('custom'); + const [description, setDescription] = useState(''); + const [modelHint, setModelHint] = useState('chat'); + const [systemPrompt, setSystemPrompt] = useState(''); + const [userTemplate, setUserTemplate] = useState(''); + const [hotkey, setHotkey] = useState(''); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!open) return; + if (editing) { + setName(editing.name); + setIcon(editing.icon); + setCategory(editing.category); + setDescription(editing.description); + setModelHint(editing.modelHint); + setSystemPrompt(editing.systemPrompt); + setUserTemplate(editing.userTemplate); + setHotkey(editing.hotkey || ''); + return; + } + + setName(''); + setIcon('โœจ'); + setCategory('custom'); + setDescription(''); + setModelHint('chat'); + setSystemPrompt(''); + setUserTemplate(''); + setHotkey(''); + }, [open, editing]); + + const canSave = useMemo(() => { + return name.trim().length > 0 && systemPrompt.trim().length > 0; + }, [name, systemPrompt]); + + if (!open) return null; + + return ( +
+
e.stopPropagation()} + > +

+ {editing ? 'Edit Quick Action' : 'Create Quick Action'} +

+ +
+ + + + + + + + + + +