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
This commit is contained in:
parent
1335d47869
commit
7ae92da16e
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"lucide-react": "^0.575.0",
|
"lucide-react": "^0.575.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
|
|||||||
@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { getConversation, getMessages } from '../../../lib/db';
|
import { getConversation, getMessages } from '../../../lib/db';
|
||||||
import type { Conversation, Message } from '../../../lib/types';
|
import type { Conversation, Message } from '../../../lib/types';
|
||||||
import { ConversationView } from '../../components/ConversationView';
|
import { ConversationView } from '../../components/ConversationView';
|
||||||
|
|
||||||
export default function ConversationPage({ params }: { params: { id: string } }) {
|
export default function ConversationPage({ params }: { params: { id: string } }) {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const [conversation, setConversation] = useState<Conversation | null>(null);
|
const [conversation, setConversation] = useState<Conversation | null>(null);
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -47,5 +49,11 @@ export default function ConversationPage({ params }: { params: { id: string } })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ConversationView conversation={conversation} initialMessages={messages} />;
|
return (
|
||||||
|
<ConversationView
|
||||||
|
conversation={conversation}
|
||||||
|
initialMessages={messages}
|
||||||
|
initialTemplate={searchParams.get('template') || ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<CommandItem[]>(() => {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 p-4"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mt-16 w-full max-w-2xl rounded-lg border border-white/10 bg-[var(--surface-card)] p-3 shadow-2xl"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={query}
|
||||||
|
onChange={e => 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();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="max-h-[60vh] space-y-1 overflow-auto">
|
||||||
|
{results.map(item => (
|
||||||
|
<button
|
||||||
|
key={`${item.type}-${item.id}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (item.type === 'qa') onSelectQuickAction(item.payload);
|
||||||
|
if (item.type === 'conversation') onSelectConversation(item.payload);
|
||||||
|
if (item.type === 'system') onSelectSystem(item.action);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left hover:bg-white/5"
|
||||||
|
>
|
||||||
|
<span className="text-base">{item.icon}</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate text-sm text-[var(--text-primary)]">{item.name}</p>
|
||||||
|
<p className="truncate text-xs text-[var(--text-tertiary)]">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
<span className="rounded border border-white/10 px-2 py-0.5 text-[10px] uppercase text-[var(--text-tertiary)]">
|
||||||
|
{item.type}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{results.length === 0 && (
|
||||||
|
<p className="px-2 py-6 text-center text-sm text-[var(--text-tertiary)]">No results</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -12,12 +12,14 @@ import { estimateTokens, getModelContextWindow } from '../../lib/format';
|
|||||||
interface ConversationViewProps {
|
interface ConversationViewProps {
|
||||||
conversation: Conversation;
|
conversation: Conversation;
|
||||||
initialMessages: Message[];
|
initialMessages: Message[];
|
||||||
|
initialTemplate?: string;
|
||||||
onConversationUpdated?: (next: Conversation) => void;
|
onConversationUpdated?: (next: Conversation) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConversationView({
|
export function ConversationView({
|
||||||
conversation,
|
conversation,
|
||||||
initialMessages,
|
initialMessages,
|
||||||
|
initialTemplate,
|
||||||
onConversationUpdated,
|
onConversationUpdated,
|
||||||
}: ConversationViewProps) {
|
}: ConversationViewProps) {
|
||||||
const [title, setTitle] = useState(conversation.title);
|
const [title, setTitle] = useState(conversation.title);
|
||||||
@ -269,6 +271,7 @@ export function ConversationView({
|
|||||||
streaming={streaming}
|
streaming={streaming}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
disabled={!selectedModel}
|
disabled={!selectedModel}
|
||||||
|
initialText={initialTemplate || ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<QuickAction['category']>('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 (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 p-4"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-2xl rounded-lg border border-white/10 bg-[var(--surface-card)] p-4"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h2 className="mb-3 text-lg font-semibold text-[var(--text-primary)]">
|
||||||
|
{editing ? 'Edit Quick Action' : 'Create Quick Action'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Name
|
||||||
|
<input
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Icon
|
||||||
|
<input
|
||||||
|
value={icon}
|
||||||
|
onChange={e => setIcon(e.target.value)}
|
||||||
|
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Category
|
||||||
|
<select
|
||||||
|
value={category}
|
||||||
|
onChange={e => setCategory(e.target.value as QuickAction['category'])}
|
||||||
|
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
|
||||||
|
>
|
||||||
|
{CATEGORIES.map(item => (
|
||||||
|
<option key={item} value={item}>
|
||||||
|
{item}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Model Hint
|
||||||
|
<select
|
||||||
|
value={modelHint}
|
||||||
|
onChange={e => setModelHint(e.target.value)}
|
||||||
|
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
|
||||||
|
>
|
||||||
|
{MODEL_HINTS.map(item => (
|
||||||
|
<option key={item} value={item}>
|
||||||
|
{item}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="md:col-span-2 text-sm text-[var(--text-secondary)]">
|
||||||
|
Description
|
||||||
|
<input
|
||||||
|
value={description}
|
||||||
|
onChange={e => setDescription(e.target.value)}
|
||||||
|
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="md:col-span-2 text-sm text-[var(--text-secondary)]">
|
||||||
|
System Prompt
|
||||||
|
<textarea
|
||||||
|
rows={5}
|
||||||
|
value={systemPrompt}
|
||||||
|
onChange={e => setSystemPrompt(e.target.value)}
|
||||||
|
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="md:col-span-2 text-sm text-[var(--text-secondary)]">
|
||||||
|
User Template
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
value={userTemplate}
|
||||||
|
onChange={e => setUserTemplate(e.target.value)}
|
||||||
|
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Hotkey (optional)
|
||||||
|
<input
|
||||||
|
value={hotkey}
|
||||||
|
onChange={e => setHotkey(e.target.value)}
|
||||||
|
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="rounded border border-white/10 px-3 py-1 text-sm text-[var(--text-secondary)] hover:bg-white/5"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!canSave || saving}
|
||||||
|
onClick={async () => {
|
||||||
|
setSaving(true);
|
||||||
|
const action: QuickAction = {
|
||||||
|
id: editing?.id || crypto.randomUUID(),
|
||||||
|
name: name.trim(),
|
||||||
|
icon: icon.trim() || '✨',
|
||||||
|
category,
|
||||||
|
description: description.trim(),
|
||||||
|
modelHint,
|
||||||
|
systemPrompt: systemPrompt.trim(),
|
||||||
|
userTemplate: userTemplate.trim(),
|
||||||
|
builtin: false,
|
||||||
|
hotkey: hotkey.trim() || undefined,
|
||||||
|
usageCount: editing?.usageCount ?? 0,
|
||||||
|
lastUsed: editing?.lastUsed,
|
||||||
|
};
|
||||||
|
await onSave(action);
|
||||||
|
setSaving(false);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
className="rounded bg-[var(--accent-primary)] px-3 py-1 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{saving ? 'Saving...' : 'Save'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -11,7 +11,7 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
UserRound,
|
UserRound,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { Conversation } from '../../lib/types';
|
import type { Conversation, QuickAction } from '../../lib/types';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { ConversationList } from './ConversationList';
|
import { ConversationList } from './ConversationList';
|
||||||
|
|
||||||
@ -21,6 +21,8 @@ interface SidebarProps {
|
|||||||
onNewConversation: () => void;
|
onNewConversation: () => void;
|
||||||
activeConversationId?: string;
|
activeConversationId?: string;
|
||||||
onSelectConversation: (id: string) => void;
|
onSelectConversation: (id: string) => void;
|
||||||
|
quickActions: QuickAction[];
|
||||||
|
onLaunchQuickAction: (qa: QuickAction) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({
|
export function Sidebar({
|
||||||
@ -29,6 +31,8 @@ export function Sidebar({
|
|||||||
onNewConversation,
|
onNewConversation,
|
||||||
activeConversationId,
|
activeConversationId,
|
||||||
onSelectConversation,
|
onSelectConversation,
|
||||||
|
quickActions,
|
||||||
|
onLaunchQuickAction,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
@ -36,6 +40,7 @@ export function Sidebar({
|
|||||||
if (!q) return conversations;
|
if (!q) return conversations;
|
||||||
return conversations.filter(c => c.title.toLowerCase().includes(q));
|
return conversations.filter(c => c.title.toLowerCase().includes(q));
|
||||||
}, [conversations, query]);
|
}, [conversations, query]);
|
||||||
|
const topQuickActions = useMemo(() => quickActions.slice(0, 5), [quickActions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
@ -67,6 +72,27 @@ export function Sidebar({
|
|||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{!collapsed && topQuickActions.length > 0 && (
|
||||||
|
<section>
|
||||||
|
<p className="mb-1 text-[10px] uppercase tracking-wide text-[var(--text-tertiary)]">
|
||||||
|
Frequently-used Quick Actions
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{topQuickActions.map(qa => (
|
||||||
|
<button
|
||||||
|
key={qa.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onLaunchQuickAction(qa)}
|
||||||
|
className="flex w-full items-center gap-2 rounded px-2 py-1 text-left text-xs text-[var(--text-secondary)] hover:bg-white/5 hover:text-[var(--text-primary)]"
|
||||||
|
>
|
||||||
|
<span>{qa.icon}</span>
|
||||||
|
<span className="truncate">{qa.name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="my-1 border-t border-white/10" />
|
<div className="my-1 border-t border-white/10" />
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-auto">
|
<div className="min-h-0 flex-1 overflow-auto">
|
||||||
|
|||||||
@ -3,15 +3,27 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { Sidebar } from './components/Sidebar';
|
import { Sidebar } from './components/Sidebar';
|
||||||
import { addConversation, listConversations } from '../lib/db';
|
import {
|
||||||
import type { Conversation } from '../lib/types';
|
addQuickAction,
|
||||||
|
addConversation,
|
||||||
|
listConversations,
|
||||||
|
listQuickActions,
|
||||||
|
seedBuiltinQuickActions,
|
||||||
|
updateQuickAction,
|
||||||
|
} from '../lib/db';
|
||||||
|
import type { Conversation, QuickAction } from '../lib/types';
|
||||||
import { migrateV3ToV4 } from '../lib/migrate';
|
import { migrateV3ToV4 } from '../lib/migrate';
|
||||||
|
import { CommandPalette } from './components/CommandPalette';
|
||||||
|
import { QuickActionEditor } from './components/QuickActionEditor';
|
||||||
|
|
||||||
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
|
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||||
|
const [quickActions, setQuickActions] = useState<QuickAction[]>([]);
|
||||||
|
const [paletteOpen, setPaletteOpen] = useState(false);
|
||||||
|
const [editorOpen, setEditorOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saved = localStorage.getItem('llm-sidebar-state');
|
const saved = localStorage.getItem('llm-sidebar-state');
|
||||||
@ -28,6 +40,15 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
||||||
|
e.preventDefault();
|
||||||
|
setPaletteOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setPaletteOpen(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', handler);
|
window.addEventListener('keydown', handler);
|
||||||
return () => window.removeEventListener('keydown', handler);
|
return () => window.removeEventListener('keydown', handler);
|
||||||
@ -36,12 +57,49 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
await migrateV3ToV4();
|
await migrateV3ToV4();
|
||||||
|
await seedBuiltinQuickActions();
|
||||||
const rows = await listConversations({ archived: false, limit: 100 });
|
const rows = await listConversations({ archived: false, limit: 100 });
|
||||||
|
const qas = await listQuickActions();
|
||||||
setConversations(rows);
|
setConversations(rows);
|
||||||
|
setQuickActions(qas);
|
||||||
};
|
};
|
||||||
void load();
|
void load();
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
|
const launchQuickAction = async (qa: QuickAction) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const conv: Conversation = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
title: qa.name,
|
||||||
|
model: qa.modelHint,
|
||||||
|
systemPrompt: qa.systemPrompt,
|
||||||
|
messageCount: 0,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
pinned: false,
|
||||||
|
archived: false,
|
||||||
|
metadata: {
|
||||||
|
totalTokens: 0,
|
||||||
|
totalPrompts: 0,
|
||||||
|
avgTokPerSec: 0,
|
||||||
|
models: qa.modelHint ? [qa.modelHint] : [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await addConversation(conv);
|
||||||
|
await updateQuickAction(qa.id, { usageCount: qa.usageCount + 1, lastUsed: now });
|
||||||
|
setConversations(prev => [conv, ...prev]);
|
||||||
|
setQuickActions(prev =>
|
||||||
|
prev
|
||||||
|
.map(item =>
|
||||||
|
item.id === qa.id ? { ...item, usageCount: item.usageCount + 1, lastUsed: now } : item
|
||||||
|
)
|
||||||
|
.sort((a, b) => b.usageCount - a.usageCount)
|
||||||
|
);
|
||||||
|
const template = encodeURIComponent(qa.userTemplate);
|
||||||
|
router.push(`/c/${conv.id}?template=${template}`);
|
||||||
|
};
|
||||||
|
|
||||||
const onNewConversation = async () => {
|
const onNewConversation = async () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const conv: Conversation = {
|
const conv: Conversation = {
|
||||||
@ -75,8 +133,38 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
|
|||||||
onNewConversation={onNewConversation}
|
onNewConversation={onNewConversation}
|
||||||
activeConversationId={activeConversationId}
|
activeConversationId={activeConversationId}
|
||||||
onSelectConversation={id => router.push(`/c/${id}`)}
|
onSelectConversation={id => router.push(`/c/${id}`)}
|
||||||
|
quickActions={quickActions}
|
||||||
|
onLaunchQuickAction={launchQuickAction}
|
||||||
/>
|
/>
|
||||||
<main className="min-w-0 flex-1">{children}</main>
|
<main className="min-w-0 flex-1">{children}</main>
|
||||||
|
|
||||||
|
<CommandPalette
|
||||||
|
open={paletteOpen}
|
||||||
|
quickActions={quickActions}
|
||||||
|
conversations={conversations}
|
||||||
|
onClose={() => setPaletteOpen(false)}
|
||||||
|
onSelectQuickAction={launchQuickAction}
|
||||||
|
onSelectConversation={conversation => router.push(`/c/${conversation.id}`)}
|
||||||
|
onSelectSystem={action => {
|
||||||
|
if (action === 'mission-control') router.push('/mission-control');
|
||||||
|
if (action === 'settings') {
|
||||||
|
setEditorOpen(true);
|
||||||
|
}
|
||||||
|
if (action === 'export') {
|
||||||
|
alert('Data export will be wired in a follow-up phase.');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QuickActionEditor
|
||||||
|
open={editorOpen}
|
||||||
|
onClose={() => setEditorOpen(false)}
|
||||||
|
onSave={async action => {
|
||||||
|
await addQuickAction(action);
|
||||||
|
const qas = await listQuickActions();
|
||||||
|
setQuickActions(qas);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type {
|
|||||||
QuickAction,
|
QuickAction,
|
||||||
ScheduledTask,
|
ScheduledTask,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { BUILTIN_QUICK_ACTIONS } from './quick-actions';
|
||||||
|
|
||||||
interface TaskRunRecord {
|
interface TaskRunRecord {
|
||||||
id?: number;
|
id?: number;
|
||||||
@ -234,3 +235,83 @@ export async function countMessages(conversationId: string): Promise<number> {
|
|||||||
const range = IDBKeyRange.bound([conversationId, 0], [conversationId, Number.MAX_SAFE_INTEGER]);
|
const range = IDBKeyRange.bound([conversationId, 0], [conversationId, Number.MAX_SAFE_INTEGER]);
|
||||||
return db.countFromIndex('messages', 'by-conversation-ts', range);
|
return db.countFromIndex('messages', 'by-conversation-ts', range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addQuickAction(action: QuickAction): Promise<void> {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.put('quickActions', action);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getQuickAction(id: string): Promise<QuickAction | undefined> {
|
||||||
|
const db = await getDb();
|
||||||
|
return db.get('quickActions', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listQuickActions(category?: QuickAction['category']): Promise<QuickAction[]> {
|
||||||
|
const db = await getDb();
|
||||||
|
let rows = await db.getAll('quickActions');
|
||||||
|
if (category) {
|
||||||
|
rows = rows.filter(row => row.category === category);
|
||||||
|
}
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
if (a.builtin !== b.builtin) return a.builtin ? -1 : 1;
|
||||||
|
return b.usageCount - a.usageCount;
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateQuickAction(
|
||||||
|
id: string,
|
||||||
|
partial: Partial<QuickAction>
|
||||||
|
): Promise<QuickAction | null> {
|
||||||
|
const db = await getDb();
|
||||||
|
const existing = await db.get('quickActions', id);
|
||||||
|
if (!existing) return null;
|
||||||
|
const updated: QuickAction = { ...existing, ...partial, id };
|
||||||
|
await db.put('quickActions', updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteQuickAction(id: string): Promise<void> {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.delete('quickActions', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedBuiltinQuickActions(): Promise<number> {
|
||||||
|
const seeded = typeof window !== 'undefined' ? localStorage.getItem('llm-qa-seeded') : 'true';
|
||||||
|
if (seeded === 'true') return 0;
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
|
const tx = db.transaction('quickActions', 'readwrite');
|
||||||
|
let inserted = 0;
|
||||||
|
|
||||||
|
for (const action of BUILTIN_QUICK_ACTIONS) {
|
||||||
|
const existing = await tx.store.get(action.id);
|
||||||
|
if (!existing) {
|
||||||
|
await tx.store.put(action);
|
||||||
|
inserted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.done;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('llm-qa-seeded', 'true');
|
||||||
|
}
|
||||||
|
return inserted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportQuickActions(): Promise<QuickAction[]> {
|
||||||
|
const rows = await listQuickActions();
|
||||||
|
return rows.filter(row => !row.builtin);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importQuickActions(actions: QuickAction[]): Promise<number> {
|
||||||
|
const db = await getDb();
|
||||||
|
let imported = 0;
|
||||||
|
for (const action of actions) {
|
||||||
|
const existing = await db.get('quickActions', action.id);
|
||||||
|
if (existing) continue;
|
||||||
|
await db.put('quickActions', action);
|
||||||
|
imported += 1;
|
||||||
|
}
|
||||||
|
return imported;
|
||||||
|
}
|
||||||
|
|||||||
337
__LOCAL_LLMs/dashboard/src/app/lib/quick-actions.ts
Normal file
337
__LOCAL_LLMs/dashboard/src/app/lib/quick-actions.ts
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
import type { QuickAction } from './types';
|
||||||
|
|
||||||
|
function qa(
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
icon: string,
|
||||||
|
category: QuickAction['category'],
|
||||||
|
modelHint: string,
|
||||||
|
description: string,
|
||||||
|
systemPrompt: string,
|
||||||
|
userTemplate: string
|
||||||
|
): QuickAction {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
icon,
|
||||||
|
category,
|
||||||
|
modelHint,
|
||||||
|
description,
|
||||||
|
systemPrompt,
|
||||||
|
userTemplate,
|
||||||
|
builtin: true,
|
||||||
|
usageCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BUILTIN_QUICK_ACTIONS: QuickAction[] = [
|
||||||
|
// Code (8)
|
||||||
|
qa(
|
||||||
|
'builtin-code-review',
|
||||||
|
'Code Review',
|
||||||
|
'🔍',
|
||||||
|
'code',
|
||||||
|
'reasoning',
|
||||||
|
'Review code for bugs, perf, and security',
|
||||||
|
'You are a senior code reviewer. Find bugs, security issues, performance issues, and maintainability improvements. Reference concrete code lines and provide fixes.',
|
||||||
|
'Review this code:\n\n{paste code}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-explain-code',
|
||||||
|
'Explain Code',
|
||||||
|
'📖',
|
||||||
|
'code',
|
||||||
|
'fast',
|
||||||
|
'Explain what code does',
|
||||||
|
'Explain the provided code in clear simple language. Break down each section and call out important patterns.',
|
||||||
|
'Explain this code:\n\n{paste code}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-write-tests',
|
||||||
|
'Write Tests',
|
||||||
|
'🧪',
|
||||||
|
'code',
|
||||||
|
'coding',
|
||||||
|
'Generate unit tests',
|
||||||
|
'You are a test engineer. Generate comprehensive unit tests with edge cases and clear test names.',
|
||||||
|
'Write tests for:\n\n{paste code}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-debug-error',
|
||||||
|
'Debug Error',
|
||||||
|
'🐛',
|
||||||
|
'code',
|
||||||
|
'reasoning',
|
||||||
|
'Root-cause debugging assistant',
|
||||||
|
'You are a debugging specialist. Identify root cause, explain why, and provide corrected code.',
|
||||||
|
'Error:\n{paste error}\n\nCode:\n{paste code}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-refactor',
|
||||||
|
'Refactor',
|
||||||
|
'♻️',
|
||||||
|
'code',
|
||||||
|
'coding',
|
||||||
|
'Refactor for readability and performance',
|
||||||
|
'Refactor the code for readability, maintainability, and performance without changing behavior.',
|
||||||
|
'Refactor this:\n\n{paste code}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-generate-code',
|
||||||
|
'Generate Code',
|
||||||
|
'⚡',
|
||||||
|
'code',
|
||||||
|
'coding',
|
||||||
|
'Generate production-ready code',
|
||||||
|
'Generate robust production-ready code with clear comments only where needed.',
|
||||||
|
'Build this:\n\n{describe requirement}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-regex-builder',
|
||||||
|
'Regex Builder',
|
||||||
|
'🔤',
|
||||||
|
'code',
|
||||||
|
'fast',
|
||||||
|
'Build and explain regex patterns',
|
||||||
|
'You are a regex expert. Build the requested regex and explain each part with test examples.',
|
||||||
|
'Create regex for:\n\n{describe pattern}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-sql-query',
|
||||||
|
'SQL Query',
|
||||||
|
'🗄️',
|
||||||
|
'code',
|
||||||
|
'coding',
|
||||||
|
'Generate optimized SQL queries',
|
||||||
|
'You are a SQL expert. Generate efficient SQL and explain indexing/performance implications.',
|
||||||
|
'Write SQL for:\n\n{describe}\n\nSchema:\n{paste schema}'
|
||||||
|
),
|
||||||
|
|
||||||
|
// Writing (6)
|
||||||
|
qa(
|
||||||
|
'builtin-summarize',
|
||||||
|
'Summarize',
|
||||||
|
'📝',
|
||||||
|
'writing',
|
||||||
|
'fast',
|
||||||
|
'Summarize content into key points',
|
||||||
|
'Summarize the content clearly with concise bullet points and key takeaways.',
|
||||||
|
'Summarize:\n\n{paste text}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-rewrite',
|
||||||
|
'Rewrite',
|
||||||
|
'✏️',
|
||||||
|
'writing',
|
||||||
|
'chat',
|
||||||
|
'Rewrite text for clarity/tone',
|
||||||
|
'Rewrite while preserving meaning and improving clarity, flow, and tone.',
|
||||||
|
'Rewrite this:\n\n{paste text}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-translate',
|
||||||
|
'Translate',
|
||||||
|
'🌐',
|
||||||
|
'writing',
|
||||||
|
'chat',
|
||||||
|
'Translate while preserving nuance',
|
||||||
|
'Translate accurately preserving tone and intent. Provide alternate phrasing if ambiguous.',
|
||||||
|
'Translate to {language}:\n\n{paste text}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-proofread',
|
||||||
|
'Proofread',
|
||||||
|
'✅',
|
||||||
|
'writing',
|
||||||
|
'fast',
|
||||||
|
'Proofread grammar and style',
|
||||||
|
'Proofread grammar, spelling, punctuation, and style. Return corrected version and notable issues.',
|
||||||
|
'Proofread:\n\n{paste text}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-draft-email',
|
||||||
|
'Draft Email',
|
||||||
|
'📧',
|
||||||
|
'writing',
|
||||||
|
'chat',
|
||||||
|
'Draft professional email',
|
||||||
|
'Draft concise professional emails with clear subject, action items, and tone.',
|
||||||
|
'Draft email to {recipient} about {topic}.'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-tech-doc',
|
||||||
|
'Technical Docs',
|
||||||
|
'📄',
|
||||||
|
'writing',
|
||||||
|
'coding',
|
||||||
|
'Write technical documentation',
|
||||||
|
'Write clear technical documentation with examples, edge cases, and concise structure.',
|
||||||
|
'Document this:\n\n{paste code or feature}'
|
||||||
|
),
|
||||||
|
|
||||||
|
// Analysis (6)
|
||||||
|
qa(
|
||||||
|
'builtin-deep-think',
|
||||||
|
'Deep Think',
|
||||||
|
'🧠',
|
||||||
|
'analysis',
|
||||||
|
'reasoning',
|
||||||
|
'Step-by-step deep analysis',
|
||||||
|
'Think carefully and step-by-step. Compare alternatives and provide recommendation.',
|
||||||
|
'Think through:\n\n{problem}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-compare-options',
|
||||||
|
'Compare Options',
|
||||||
|
'⚖️',
|
||||||
|
'analysis',
|
||||||
|
'reasoning',
|
||||||
|
'Compare alternatives with trade-offs',
|
||||||
|
'Compare options objectively with pros/cons matrix and recommendation.',
|
||||||
|
'Compare:\n\n{option A}\nvs\n{option B}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-eli5',
|
||||||
|
'Explain Like I’m 5',
|
||||||
|
'👶',
|
||||||
|
'analysis',
|
||||||
|
'chat',
|
||||||
|
'Explain complex ideas simply',
|
||||||
|
'Explain complex concepts to a beginner using analogies and plain language.',
|
||||||
|
'Explain simply:\n\n{topic}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-fact-check',
|
||||||
|
'Fact Check',
|
||||||
|
'🔎',
|
||||||
|
'analysis',
|
||||||
|
'reasoning',
|
||||||
|
'Check claims and caveats',
|
||||||
|
'Evaluate factual claims. Separate confirmed facts, likely true, and uncertain points.',
|
||||||
|
'Fact-check:\n\n{claim}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-risk-review',
|
||||||
|
'Risk Review',
|
||||||
|
'⚠️',
|
||||||
|
'analysis',
|
||||||
|
'reasoning',
|
||||||
|
'Analyze implementation risks',
|
||||||
|
'Identify technical, security, and operational risks. Provide mitigations and severity.',
|
||||||
|
'Review risks for:\n\n{proposal}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-root-cause',
|
||||||
|
'Root Cause Analysis',
|
||||||
|
'🧩',
|
||||||
|
'analysis',
|
||||||
|
'reasoning',
|
||||||
|
'Find root causes systematically',
|
||||||
|
'Perform root cause analysis and provide 5 whys plus actionable fixes.',
|
||||||
|
'Analyze root cause:\n\n{incident description}'
|
||||||
|
),
|
||||||
|
|
||||||
|
// Creative (5)
|
||||||
|
qa(
|
||||||
|
'builtin-brainstorm',
|
||||||
|
'Brainstorm',
|
||||||
|
'💡',
|
||||||
|
'creative',
|
||||||
|
'chat',
|
||||||
|
'Generate diverse ideas',
|
||||||
|
'Generate diverse ideas including practical and unconventional options.',
|
||||||
|
'Brainstorm ideas for:\n\n{topic}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-role-play',
|
||||||
|
'Role Play',
|
||||||
|
'🎭',
|
||||||
|
'creative',
|
||||||
|
'chat',
|
||||||
|
'Role-play scenario',
|
||||||
|
'Stay in character and respond consistently with persona and context.',
|
||||||
|
'Role-play:\n\n{scenario}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-story-writer',
|
||||||
|
'Story Writer',
|
||||||
|
'📚',
|
||||||
|
'creative',
|
||||||
|
'chat',
|
||||||
|
'Write story content',
|
||||||
|
'Write engaging story content with strong pacing and vivid details.',
|
||||||
|
'Write a story about:\n\n{premise}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-name-generator',
|
||||||
|
'Name Generator',
|
||||||
|
'🏷️',
|
||||||
|
'creative',
|
||||||
|
'fast',
|
||||||
|
'Generate names with rationale',
|
||||||
|
'Generate creative names and explain why each name works.',
|
||||||
|
'Generate names for:\n\n{product or concept}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-pitch-maker',
|
||||||
|
'Pitch Maker',
|
||||||
|
'🎤',
|
||||||
|
'creative',
|
||||||
|
'chat',
|
||||||
|
'Create a concise pitch',
|
||||||
|
'Craft a concise compelling pitch with hook, value, and call to action.',
|
||||||
|
'Pitch this idea:\n\n{idea}'
|
||||||
|
),
|
||||||
|
|
||||||
|
// DevOps (5)
|
||||||
|
qa(
|
||||||
|
'builtin-shell-command',
|
||||||
|
'Shell Command',
|
||||||
|
'💻',
|
||||||
|
'devops',
|
||||||
|
'coding',
|
||||||
|
'Generate safe shell commands',
|
||||||
|
'Generate safe macOS zsh commands. Warn explicitly for destructive commands.',
|
||||||
|
'Write command to:\n\n{task}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-docker-k8s',
|
||||||
|
'Docker/K8s',
|
||||||
|
'🐳',
|
||||||
|
'devops',
|
||||||
|
'coding',
|
||||||
|
'Generate Docker/K8s configs',
|
||||||
|
'Generate secure Docker/K8s config with sensible defaults and brief rationale.',
|
||||||
|
'Generate config for:\n\n{service requirement}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-git-help',
|
||||||
|
'Git Help',
|
||||||
|
'🌿',
|
||||||
|
'devops',
|
||||||
|
'fast',
|
||||||
|
'Provide git commands for scenarios',
|
||||||
|
'Provide exact git commands and explain each command briefly.',
|
||||||
|
'Help with git scenario:\n\n{scenario}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-json-yaml',
|
||||||
|
'JSON/YAML Tool',
|
||||||
|
'📋',
|
||||||
|
'devops',
|
||||||
|
'fast',
|
||||||
|
'Convert/validate data formats',
|
||||||
|
'Convert and validate JSON/YAML with clear errors and fixed output.',
|
||||||
|
'Convert/validate:\n\n{data}'
|
||||||
|
),
|
||||||
|
qa(
|
||||||
|
'builtin-release-notes',
|
||||||
|
'Release Notes',
|
||||||
|
'🚀',
|
||||||
|
'devops',
|
||||||
|
'chat',
|
||||||
|
'Generate release notes from changes',
|
||||||
|
'Generate concise release notes grouped by feature/fix/chore.',
|
||||||
|
'Create release notes from:\n\n{change log}'
|
||||||
|
),
|
||||||
|
];
|
||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@ -71,6 +71,9 @@ importers:
|
|||||||
'@types/react-syntax-highlighter':
|
'@types/react-syntax-highlighter':
|
||||||
specifier: ^15.5.13
|
specifier: ^15.5.13
|
||||||
version: 15.5.13
|
version: 15.5.13
|
||||||
|
fuse.js:
|
||||||
|
specifier: ^7.1.0
|
||||||
|
version: 7.1.0
|
||||||
idb:
|
idb:
|
||||||
specifier: ^8.0.3
|
specifier: ^8.0.3
|
||||||
version: 8.0.3
|
version: 8.0.3
|
||||||
@ -3983,6 +3986,13 @@ packages:
|
|||||||
integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==,
|
integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fuse.js@7.1.0:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==,
|
||||||
|
}
|
||||||
|
engines: { node: '>=10' }
|
||||||
|
|
||||||
generator-function@2.0.1:
|
generator-function@2.0.1:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -9581,6 +9591,8 @@ snapshots:
|
|||||||
|
|
||||||
functions-have-names@1.2.3: {}
|
functions-have-names@1.2.3: {}
|
||||||
|
|
||||||
|
fuse.js@7.1.0: {}
|
||||||
|
|
||||||
generator-function@2.0.1: {}
|
generator-function@2.0.1: {}
|
||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user