feat(local-llm): Phase C custom agents (C1-C5)
- add built-in agents library (10 seeded agents) - add agent CRUD/seeding/export/import helpers in db layer - seed agents on workspace load - add agent strip in sidebar and launch-from-agent flow - add command palette support for agent entries - add agent conversation wiring (agentId, systemPrompt, welcome message) - render agent badge in conversation header - add example prompt chips in input bar for agent conversations - add AgentEditor modal for creating/updating custom agents
This commit is contained in:
parent
f289099461
commit
d18b695029
@ -3,13 +3,14 @@
|
|||||||
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 { useSearchParams } from 'next/navigation';
|
||||||
import { getConversation, getMessages } from '../../../lib/db';
|
import { getAgent, getConversation, getMessages } from '../../../lib/db';
|
||||||
import type { Conversation, Message } from '../../../lib/types';
|
import type { Agent, 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 searchParams = useSearchParams();
|
||||||
const [conversation, setConversation] = useState<Conversation | null>(null);
|
const [conversation, setConversation] = useState<Conversation | null>(null);
|
||||||
|
const [agent, setAgent] = useState<Agent | null>(null);
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@ -21,7 +22,9 @@ export default function ConversationPage({ params }: { params: { id: string } })
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const msgRows = await getMessages(params.id, { limit: 200 });
|
const msgRows = await getMessages(params.id, { limit: 200 });
|
||||||
|
const agentRow = row.agentId ? await getAgent(row.agentId) : undefined;
|
||||||
setConversation(row);
|
setConversation(row);
|
||||||
|
setAgent(agentRow || null);
|
||||||
setMessages(msgRows);
|
setMessages(msgRows);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@ -54,6 +57,7 @@ export default function ConversationPage({ params }: { params: { id: string } })
|
|||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
initialMessages={messages}
|
initialMessages={messages}
|
||||||
initialTemplate={searchParams.get('template') || ''}
|
initialTemplate={searchParams.get('template') || ''}
|
||||||
|
agent={agent || undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,232 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import type { Agent, AgentTool } from '../../lib/types';
|
||||||
|
|
||||||
|
interface AgentEditorProps {
|
||||||
|
open: boolean;
|
||||||
|
editing?: Agent | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (agent: Agent) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOL_OPTIONS: AgentTool[] = ['file_read', 'vision', 'voice_input'];
|
||||||
|
|
||||||
|
export function AgentEditor({ open, editing, onClose, onSave }: AgentEditorProps) {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [icon, setIcon] = useState('🤖');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [model, setModel] = useState('chat');
|
||||||
|
const [temperature, setTemperature] = useState<number>(0.7);
|
||||||
|
const [systemPrompt, setSystemPrompt] = useState('');
|
||||||
|
const [welcomeMessage, setWelcomeMessage] = useState('');
|
||||||
|
const [examplePrompts, setExamplePrompts] = useState('');
|
||||||
|
const [responseFormat, setResponseFormat] = useState<Agent['responseFormat']>('markdown');
|
||||||
|
const [tools, setTools] = useState<AgentTool[]>(['file_read']);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
if (editing) {
|
||||||
|
setName(editing.name);
|
||||||
|
setIcon(editing.icon);
|
||||||
|
setDescription(editing.description);
|
||||||
|
setModel(editing.model);
|
||||||
|
setTemperature(editing.temperature ?? 0.7);
|
||||||
|
setSystemPrompt(editing.systemPrompt);
|
||||||
|
setWelcomeMessage(editing.welcomeMessage || '');
|
||||||
|
setExamplePrompts((editing.examplePrompts || []).join('\n'));
|
||||||
|
setResponseFormat(editing.responseFormat || 'markdown');
|
||||||
|
setTools(editing.tools || ['file_read']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setName('');
|
||||||
|
setIcon('🤖');
|
||||||
|
setDescription('');
|
||||||
|
setModel('chat');
|
||||||
|
setTemperature(0.7);
|
||||||
|
setSystemPrompt('');
|
||||||
|
setWelcomeMessage('');
|
||||||
|
setExamplePrompts('');
|
||||||
|
setResponseFormat('markdown');
|
||||||
|
setTools(['file_read']);
|
||||||
|
}, [open, editing]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
const canSave = name.trim().length > 0 && systemPrompt.trim().length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[70] flex items-center justify-center bg-black/60 p-4"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-3xl 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 Agent' : 'Create Agent'}
|
||||||
|
</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)] md:col-span-2">
|
||||||
|
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="text-sm text-[var(--text-secondary)]">
|
||||||
|
Model
|
||||||
|
<input
|
||||||
|
value={model}
|
||||||
|
onChange={e => setModel(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)]">
|
||||||
|
Temperature ({temperature.toFixed(1)})
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={2}
|
||||||
|
step={0.1}
|
||||||
|
value={temperature}
|
||||||
|
onChange={e => setTemperature(Number(e.target.value))}
|
||||||
|
className="mt-2 w-full"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="text-sm text-[var(--text-secondary)] md:col-span-2">
|
||||||
|
System Prompt
|
||||||
|
<textarea
|
||||||
|
rows={6}
|
||||||
|
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="text-sm text-[var(--text-secondary)] md:col-span-2">
|
||||||
|
Welcome Message
|
||||||
|
<input
|
||||||
|
value={welcomeMessage}
|
||||||
|
onChange={e => setWelcomeMessage(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)] md:col-span-2">
|
||||||
|
Example Prompts (one per line)
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
value={examplePrompts}
|
||||||
|
onChange={e => setExamplePrompts(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)]">
|
||||||
|
Response Format
|
||||||
|
<select
|
||||||
|
value={responseFormat}
|
||||||
|
onChange={e => setResponseFormat(e.target.value as Agent['responseFormat'])}
|
||||||
|
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
|
||||||
|
>
|
||||||
|
<option value="markdown">markdown</option>
|
||||||
|
<option value="json">json</option>
|
||||||
|
<option value="code">code</option>
|
||||||
|
<option value="plain">plain</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Tools
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{TOOL_OPTIONS.map(tool => (
|
||||||
|
<label key={tool} className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={tools.includes(tool)}
|
||||||
|
onChange={e => {
|
||||||
|
setTools(prev => {
|
||||||
|
if (e.target.checked) return Array.from(new Set([...prev, tool]));
|
||||||
|
return prev.filter(item => item !== tool);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>{tool}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 agent: Agent = {
|
||||||
|
id: editing?.id || crypto.randomUUID(),
|
||||||
|
name: name.trim(),
|
||||||
|
icon: icon.trim() || '🤖',
|
||||||
|
description: description.trim(),
|
||||||
|
model: model.trim() || 'chat',
|
||||||
|
systemPrompt: systemPrompt.trim(),
|
||||||
|
temperature,
|
||||||
|
tools,
|
||||||
|
welcomeMessage: welcomeMessage.trim() || undefined,
|
||||||
|
examplePrompts: examplePrompts
|
||||||
|
.split('\n')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
constraints: editing?.constraints,
|
||||||
|
responseFormat,
|
||||||
|
builtin: false,
|
||||||
|
conversationCount: editing?.conversationCount ?? 0,
|
||||||
|
createdAt: editing?.createdAt ?? Date.now(),
|
||||||
|
};
|
||||||
|
await onSave(agent);
|
||||||
|
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 Agent'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import type { Conversation, QuickAction } from '../../lib/types';
|
import type { Agent, Conversation, QuickAction } from '../../lib/types';
|
||||||
|
|
||||||
type CommandItem =
|
type CommandItem =
|
||||||
| {
|
| {
|
||||||
@ -13,6 +13,7 @@ type CommandItem =
|
|||||||
icon: string;
|
icon: string;
|
||||||
payload: QuickAction;
|
payload: QuickAction;
|
||||||
}
|
}
|
||||||
|
| { type: 'agent'; id: string; name: string; description: string; icon: string; payload: Agent }
|
||||||
| {
|
| {
|
||||||
type: 'conversation';
|
type: 'conversation';
|
||||||
id: string;
|
id: string;
|
||||||
@ -33,9 +34,11 @@ type CommandItem =
|
|||||||
interface CommandPaletteProps {
|
interface CommandPaletteProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
quickActions: QuickAction[];
|
quickActions: QuickAction[];
|
||||||
|
agents: Agent[];
|
||||||
conversations: Conversation[];
|
conversations: Conversation[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSelectQuickAction: (qa: QuickAction) => void;
|
onSelectQuickAction: (qa: QuickAction) => void;
|
||||||
|
onSelectAgent: (agent: Agent) => void;
|
||||||
onSelectConversation: (conversation: Conversation) => void;
|
onSelectConversation: (conversation: Conversation) => void;
|
||||||
onSelectSystem: (action: 'mission-control' | 'settings' | 'export') => void;
|
onSelectSystem: (action: 'mission-control' | 'settings' | 'export') => void;
|
||||||
}
|
}
|
||||||
@ -43,9 +46,11 @@ interface CommandPaletteProps {
|
|||||||
export function CommandPalette({
|
export function CommandPalette({
|
||||||
open,
|
open,
|
||||||
quickActions,
|
quickActions,
|
||||||
|
agents,
|
||||||
conversations,
|
conversations,
|
||||||
onClose,
|
onClose,
|
||||||
onSelectQuickAction,
|
onSelectQuickAction,
|
||||||
|
onSelectAgent,
|
||||||
onSelectConversation,
|
onSelectConversation,
|
||||||
onSelectSystem,
|
onSelectSystem,
|
||||||
}: CommandPaletteProps) {
|
}: CommandPaletteProps) {
|
||||||
@ -61,6 +66,15 @@ export function CommandPalette({
|
|||||||
payload: qa,
|
payload: qa,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const agentItems: CommandItem[] = agents.map(agent => ({
|
||||||
|
type: 'agent',
|
||||||
|
id: agent.id,
|
||||||
|
name: agent.name,
|
||||||
|
description: agent.description,
|
||||||
|
icon: agent.icon,
|
||||||
|
payload: agent,
|
||||||
|
}));
|
||||||
|
|
||||||
const convItems: CommandItem[] = conversations.slice(0, 20).map(conv => ({
|
const convItems: CommandItem[] = conversations.slice(0, 20).map(conv => ({
|
||||||
type: 'conversation',
|
type: 'conversation',
|
||||||
id: conv.id,
|
id: conv.id,
|
||||||
@ -97,8 +111,8 @@ export function CommandPalette({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return [...qaItems, ...convItems, ...systemItems];
|
return [...qaItems, ...agentItems, ...convItems, ...systemItems];
|
||||||
}, [quickActions, conversations]);
|
}, [quickActions, agents, conversations]);
|
||||||
|
|
||||||
const results = useMemo(() => {
|
const results = useMemo(() => {
|
||||||
if (!query.trim()) return items.slice(0, 15);
|
if (!query.trim()) return items.slice(0, 15);
|
||||||
@ -143,6 +157,7 @@ export function CommandPalette({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item.type === 'qa') onSelectQuickAction(item.payload);
|
if (item.type === 'qa') onSelectQuickAction(item.payload);
|
||||||
|
if (item.type === 'agent') onSelectAgent(item.payload);
|
||||||
if (item.type === 'conversation') onSelectConversation(item.payload);
|
if (item.type === 'conversation') onSelectConversation(item.payload);
|
||||||
if (item.type === 'system') onSelectSystem(item.action);
|
if (item.type === 'system') onSelectSystem(item.action);
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
import type { Conversation, Message, OllamaData } from '../../lib/types';
|
import type { Agent, Conversation, Message, OllamaData } from '../../lib/types';
|
||||||
import { addMessage, updateConversation } from '../../lib/db';
|
import { addMessage, updateConversation } from '../../lib/db';
|
||||||
import { InputBar } from './InputBar';
|
import { InputBar } from './InputBar';
|
||||||
import { MessageThread } from './MessageThread';
|
import { MessageThread } from './MessageThread';
|
||||||
@ -13,6 +13,7 @@ interface ConversationViewProps {
|
|||||||
conversation: Conversation;
|
conversation: Conversation;
|
||||||
initialMessages: Message[];
|
initialMessages: Message[];
|
||||||
initialTemplate?: string;
|
initialTemplate?: string;
|
||||||
|
agent?: Agent;
|
||||||
onConversationUpdated?: (next: Conversation) => void;
|
onConversationUpdated?: (next: Conversation) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ export function ConversationView({
|
|||||||
conversation,
|
conversation,
|
||||||
initialMessages,
|
initialMessages,
|
||||||
initialTemplate,
|
initialTemplate,
|
||||||
|
agent,
|
||||||
onConversationUpdated,
|
onConversationUpdated,
|
||||||
}: ConversationViewProps) {
|
}: ConversationViewProps) {
|
||||||
const [title, setTitle] = useState(conversation.title);
|
const [title, setTitle] = useState(conversation.title);
|
||||||
@ -228,6 +230,12 @@ export function ConversationView({
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-lg font-semibold text-[var(--text-primary)]">{title}</h1>
|
<h1 className="text-lg font-semibold text-[var(--text-primary)]">{title}</h1>
|
||||||
<p className="text-xs text-[var(--text-tertiary)]">{conversation.id}</p>
|
<p className="text-xs text-[var(--text-tertiary)]">{conversation.id}</p>
|
||||||
|
{agent ? (
|
||||||
|
<p className="mt-1 inline-flex items-center gap-1 rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-0.5 text-[10px] text-[var(--text-secondary)]">
|
||||||
|
<span>{agent.icon}</span>
|
||||||
|
<span>{agent.name}</span>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -272,6 +280,7 @@ export function ConversationView({
|
|||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
disabled={!selectedModel}
|
disabled={!selectedModel}
|
||||||
initialText={initialTemplate || ''}
|
initialText={initialTemplate || ''}
|
||||||
|
examplePrompts={agent?.examplePrompts || []}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,6 +9,7 @@ interface InputBarProps {
|
|||||||
streaming?: boolean;
|
streaming?: boolean;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
|
examplePrompts?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputBar({
|
export function InputBar({
|
||||||
@ -17,6 +18,7 @@ export function InputBar({
|
|||||||
streaming,
|
streaming,
|
||||||
onCancel,
|
onCancel,
|
||||||
initialText = '',
|
initialText = '',
|
||||||
|
examplePrompts = [],
|
||||||
}: InputBarProps) {
|
}: InputBarProps) {
|
||||||
const [text, setText] = useState(initialText);
|
const [text, setText] = useState(initialText);
|
||||||
|
|
||||||
@ -33,6 +35,20 @@ export function InputBar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-white/10 bg-[var(--bg-elevated)] p-3">
|
<div className="border-t border-white/10 bg-[var(--bg-elevated)] p-3">
|
||||||
|
{examplePrompts.length > 0 && !text.trim() && (
|
||||||
|
<div className="mb-2 flex flex-wrap gap-1">
|
||||||
|
{examplePrompts.slice(0, 4).map(prompt => (
|
||||||
|
<button
|
||||||
|
key={prompt}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setText(prompt)}
|
||||||
|
className="rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||||
|
>
|
||||||
|
{prompt}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-end gap-2">
|
<div className="flex items-end gap-2">
|
||||||
<textarea
|
<textarea
|
||||||
value={text}
|
value={text}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
UserRound,
|
UserRound,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { Conversation, QuickAction } from '../../lib/types';
|
import type { Agent, Conversation, QuickAction } from '../../lib/types';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { ConversationList } from './ConversationList';
|
import { ConversationList } from './ConversationList';
|
||||||
|
|
||||||
@ -23,6 +23,10 @@ interface SidebarProps {
|
|||||||
onSelectConversation: (id: string) => void;
|
onSelectConversation: (id: string) => void;
|
||||||
quickActions: QuickAction[];
|
quickActions: QuickAction[];
|
||||||
onLaunchQuickAction: (qa: QuickAction) => void;
|
onLaunchQuickAction: (qa: QuickAction) => void;
|
||||||
|
agents: Agent[];
|
||||||
|
onLaunchAgent: (agent: Agent) => void;
|
||||||
|
onOpenQuickActionEditor: () => void;
|
||||||
|
onOpenAgentEditor: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({
|
export function Sidebar({
|
||||||
@ -33,6 +37,10 @@ export function Sidebar({
|
|||||||
onSelectConversation,
|
onSelectConversation,
|
||||||
quickActions,
|
quickActions,
|
||||||
onLaunchQuickAction,
|
onLaunchQuickAction,
|
||||||
|
agents,
|
||||||
|
onLaunchAgent,
|
||||||
|
onOpenQuickActionEditor,
|
||||||
|
onOpenAgentEditor,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
@ -41,6 +49,7 @@ export function Sidebar({
|
|||||||
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]);
|
const topQuickActions = useMemo(() => quickActions.slice(0, 5), [quickActions]);
|
||||||
|
const topAgents = useMemo(() => agents.slice(0, 6), [agents]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
@ -63,8 +72,14 @@ export function Sidebar({
|
|||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
icon={<Sparkles size={16} />}
|
icon={<Sparkles size={16} />}
|
||||||
label="Quick Actions"
|
label="Quick Actions"
|
||||||
|
onClick={onOpenQuickActionEditor}
|
||||||
|
/>
|
||||||
|
<SidebarNavItem
|
||||||
|
collapsed={collapsed}
|
||||||
|
icon={<UserRound size={16} />}
|
||||||
|
label="My Agents"
|
||||||
|
onClick={onOpenAgentEditor}
|
||||||
/>
|
/>
|
||||||
<SidebarNavItem collapsed={collapsed} icon={<UserRound size={16} />} label="My Agents" />
|
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
icon={<FolderKanban size={16} />}
|
icon={<FolderKanban size={16} />}
|
||||||
@ -72,6 +87,27 @@ export function Sidebar({
|
|||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{!collapsed && topAgents.length > 0 && (
|
||||||
|
<section>
|
||||||
|
<p className="mb-1 text-[10px] uppercase tracking-wide text-[var(--text-tertiary)]">
|
||||||
|
My Agents
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{topAgents.map(agent => (
|
||||||
|
<button
|
||||||
|
key={agent.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onLaunchAgent(agent)}
|
||||||
|
className="rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||||
|
title={`${agent.name}: ${agent.description}`}
|
||||||
|
>
|
||||||
|
{agent.icon} {agent.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{!collapsed && topQuickActions.length > 0 && (
|
{!collapsed && topQuickActions.length > 0 && (
|
||||||
<section>
|
<section>
|
||||||
<p className="mb-1 text-[10px] uppercase tracking-wide text-[var(--text-tertiary)]">
|
<p className="mb-1 text-[10px] uppercase tracking-wide text-[var(--text-tertiary)]">
|
||||||
@ -146,14 +182,17 @@ function SidebarNavItem({
|
|||||||
collapsed,
|
collapsed,
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
|
onClick?: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
className="flex w-full items-center gap-2 rounded px-2 py-1 text-[var(--text-secondary)] hover:bg-white/5 hover:text-[var(--text-primary)]"
|
className="flex w-full items-center gap-2 rounded px-2 py-1 text-[var(--text-secondary)] hover:bg-white/5 hover:text-[var(--text-primary)]"
|
||||||
title={label}
|
title={label}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -4,17 +4,23 @@ 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 {
|
import {
|
||||||
|
addAgent,
|
||||||
addQuickAction,
|
addQuickAction,
|
||||||
addConversation,
|
addConversation,
|
||||||
|
addMessage,
|
||||||
|
listAgents,
|
||||||
listConversations,
|
listConversations,
|
||||||
listQuickActions,
|
listQuickActions,
|
||||||
|
seedBuiltinAgents,
|
||||||
seedBuiltinQuickActions,
|
seedBuiltinQuickActions,
|
||||||
|
updateAgent,
|
||||||
updateQuickAction,
|
updateQuickAction,
|
||||||
} from '../lib/db';
|
} from '../lib/db';
|
||||||
import type { Conversation, QuickAction } from '../lib/types';
|
import type { Agent, Conversation, QuickAction } from '../lib/types';
|
||||||
import { migrateV3ToV4 } from '../lib/migrate';
|
import { migrateV3ToV4 } from '../lib/migrate';
|
||||||
import { CommandPalette } from './components/CommandPalette';
|
import { CommandPalette } from './components/CommandPalette';
|
||||||
import { QuickActionEditor } from './components/QuickActionEditor';
|
import { QuickActionEditor } from './components/QuickActionEditor';
|
||||||
|
import { AgentEditor } from './components/AgentEditor';
|
||||||
|
|
||||||
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
|
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -22,8 +28,10 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
|
|||||||
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 [quickActions, setQuickActions] = useState<QuickAction[]>([]);
|
||||||
|
const [agents, setAgents] = useState<Agent[]>([]);
|
||||||
const [paletteOpen, setPaletteOpen] = useState(false);
|
const [paletteOpen, setPaletteOpen] = useState(false);
|
||||||
const [editorOpen, setEditorOpen] = useState(false);
|
const [editorOpen, setEditorOpen] = useState(false);
|
||||||
|
const [agentEditorOpen, setAgentEditorOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saved = localStorage.getItem('llm-sidebar-state');
|
const saved = localStorage.getItem('llm-sidebar-state');
|
||||||
@ -58,14 +66,61 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
|
|||||||
const load = async () => {
|
const load = async () => {
|
||||||
await migrateV3ToV4();
|
await migrateV3ToV4();
|
||||||
await seedBuiltinQuickActions();
|
await seedBuiltinQuickActions();
|
||||||
|
await seedBuiltinAgents();
|
||||||
const rows = await listConversations({ archived: false, limit: 100 });
|
const rows = await listConversations({ archived: false, limit: 100 });
|
||||||
const qas = await listQuickActions();
|
const qas = await listQuickActions();
|
||||||
|
const ags = await listAgents();
|
||||||
setConversations(rows);
|
setConversations(rows);
|
||||||
setQuickActions(qas);
|
setQuickActions(qas);
|
||||||
|
setAgents(ags);
|
||||||
};
|
};
|
||||||
void load();
|
void load();
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
|
const launchAgent = async (agent: Agent) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const conv: Conversation = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
title: agent.name,
|
||||||
|
model: agent.model,
|
||||||
|
agentId: agent.id,
|
||||||
|
systemPrompt: agent.systemPrompt,
|
||||||
|
messageCount: 1,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
pinned: false,
|
||||||
|
archived: false,
|
||||||
|
metadata: {
|
||||||
|
totalTokens: 0,
|
||||||
|
totalPrompts: 0,
|
||||||
|
avgTokPerSec: 0,
|
||||||
|
models: agent.model ? [agent.model] : [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await addConversation(conv);
|
||||||
|
await addMessage({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
conversationId: conv.id,
|
||||||
|
role: 'assistant',
|
||||||
|
content: agent.welcomeMessage || `Hi, I am ${agent.name}. How can I help?`,
|
||||||
|
timestamp: now,
|
||||||
|
model: agent.model,
|
||||||
|
});
|
||||||
|
await updateAgent(agent.id, { conversationCount: agent.conversationCount + 1 });
|
||||||
|
|
||||||
|
setConversations(prev => [conv, ...prev]);
|
||||||
|
setAgents(prev =>
|
||||||
|
prev
|
||||||
|
.map(item =>
|
||||||
|
item.id === agent.id ? { ...item, conversationCount: item.conversationCount + 1 } : item
|
||||||
|
)
|
||||||
|
.sort((a, b) => b.conversationCount - a.conversationCount)
|
||||||
|
);
|
||||||
|
|
||||||
|
router.push(`/c/${conv.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
const launchQuickAction = async (qa: QuickAction) => {
|
const launchQuickAction = async (qa: QuickAction) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const conv: Conversation = {
|
const conv: Conversation = {
|
||||||
@ -135,20 +190,26 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
|
|||||||
onSelectConversation={id => router.push(`/c/${id}`)}
|
onSelectConversation={id => router.push(`/c/${id}`)}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
onLaunchQuickAction={launchQuickAction}
|
onLaunchQuickAction={launchQuickAction}
|
||||||
|
agents={agents}
|
||||||
|
onLaunchAgent={launchAgent}
|
||||||
|
onOpenQuickActionEditor={() => setEditorOpen(true)}
|
||||||
|
onOpenAgentEditor={() => setAgentEditorOpen(true)}
|
||||||
/>
|
/>
|
||||||
<main className="min-w-0 flex-1">{children}</main>
|
<main className="min-w-0 flex-1">{children}</main>
|
||||||
|
|
||||||
<CommandPalette
|
<CommandPalette
|
||||||
open={paletteOpen}
|
open={paletteOpen}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
|
agents={agents}
|
||||||
conversations={conversations}
|
conversations={conversations}
|
||||||
onClose={() => setPaletteOpen(false)}
|
onClose={() => setPaletteOpen(false)}
|
||||||
onSelectQuickAction={launchQuickAction}
|
onSelectQuickAction={launchQuickAction}
|
||||||
|
onSelectAgent={launchAgent}
|
||||||
onSelectConversation={conversation => router.push(`/c/${conversation.id}`)}
|
onSelectConversation={conversation => router.push(`/c/${conversation.id}`)}
|
||||||
onSelectSystem={action => {
|
onSelectSystem={action => {
|
||||||
if (action === 'mission-control') router.push('/mission-control');
|
if (action === 'mission-control') router.push('/mission-control');
|
||||||
if (action === 'settings') {
|
if (action === 'settings') {
|
||||||
setEditorOpen(true);
|
setAgentEditorOpen(true);
|
||||||
}
|
}
|
||||||
if (action === 'export') {
|
if (action === 'export') {
|
||||||
alert('Data export will be wired in a follow-up phase.');
|
alert('Data export will be wired in a follow-up phase.');
|
||||||
@ -165,6 +226,21 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
|
|||||||
setQuickActions(qas);
|
setQuickActions(qas);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AgentEditor
|
||||||
|
open={agentEditorOpen}
|
||||||
|
onClose={() => setAgentEditorOpen(false)}
|
||||||
|
onSave={async agent => {
|
||||||
|
const existing = agents.find(item => item.id === agent.id);
|
||||||
|
if (existing) {
|
||||||
|
await updateAgent(agent.id, agent);
|
||||||
|
} else {
|
||||||
|
await addAgent(agent);
|
||||||
|
}
|
||||||
|
const ags = await listAgents();
|
||||||
|
setAgents(ags);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
130
__LOCAL_LLMs/dashboard/src/app/lib/agents.ts
Normal file
130
__LOCAL_LLMs/dashboard/src/app/lib/agents.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import type { Agent } from './types';
|
||||||
|
|
||||||
|
function agent(
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
icon: string,
|
||||||
|
model: string,
|
||||||
|
description: string,
|
||||||
|
systemPrompt: string,
|
||||||
|
welcomeMessage: string,
|
||||||
|
examplePrompts: string[]
|
||||||
|
): Agent {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
icon,
|
||||||
|
description,
|
||||||
|
model,
|
||||||
|
systemPrompt,
|
||||||
|
tools: ['file_read'],
|
||||||
|
welcomeMessage,
|
||||||
|
examplePrompts,
|
||||||
|
builtin: true,
|
||||||
|
conversationCount: 0,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BUILTIN_AGENTS: Agent[] = [
|
||||||
|
agent(
|
||||||
|
'agent-code-pilot',
|
||||||
|
'Code Pilot',
|
||||||
|
'👨✈️',
|
||||||
|
'coding',
|
||||||
|
'Full-stack coding copilot',
|
||||||
|
'You are Code Pilot, a senior software engineer. Prioritize correctness, maintainability, and clear implementation details.',
|
||||||
|
'Welcome! Share your code or requirement and I will help you implement it safely.',
|
||||||
|
['Review this function', 'Generate endpoint with validation', 'Refactor for readability']
|
||||||
|
),
|
||||||
|
agent(
|
||||||
|
'agent-writing-editor',
|
||||||
|
'Writing Editor',
|
||||||
|
'📝',
|
||||||
|
'chat',
|
||||||
|
'Improve writing quality and tone',
|
||||||
|
'You are a writing editor. Improve clarity, tone, flow, and grammar while preserving intent.',
|
||||||
|
'Welcome! Paste text and I will rewrite it for clarity and impact.',
|
||||||
|
['Rewrite this email', 'Proofread this paragraph', 'Make this concise']
|
||||||
|
),
|
||||||
|
agent(
|
||||||
|
'agent-deep-thinker',
|
||||||
|
'Deep Thinker',
|
||||||
|
'🧠',
|
||||||
|
'reasoning',
|
||||||
|
'Step-by-step reasoning specialist',
|
||||||
|
'You are a deliberate reasoning assistant. Think step by step and call out tradeoffs before concluding.',
|
||||||
|
'Let’s reason through the problem together, step by step.',
|
||||||
|
['Analyze this architecture decision', 'Compare these options', 'Root-cause this issue']
|
||||||
|
),
|
||||||
|
agent(
|
||||||
|
'agent-shell-wizard',
|
||||||
|
'Shell Wizard',
|
||||||
|
'💻',
|
||||||
|
'coding',
|
||||||
|
'macOS/zsh command expert',
|
||||||
|
'You are a shell command expert for macOS and zsh. Always flag destructive commands and explain flags.',
|
||||||
|
'Need a command? Tell me the task and I will generate a safe command.',
|
||||||
|
['Create archive of logs', 'Find large files', 'Bulk rename files']
|
||||||
|
),
|
||||||
|
agent(
|
||||||
|
'agent-data-analyst',
|
||||||
|
'Data Analyst',
|
||||||
|
'📊',
|
||||||
|
'reasoning',
|
||||||
|
'Data and SQL analysis specialist',
|
||||||
|
'You are a data analyst. Provide structured analysis, assumptions, and recommended actions.',
|
||||||
|
'Share data or a question and I will analyze it clearly.',
|
||||||
|
['Write this SQL query', 'Analyze this CSV trend', 'Summarize metrics']
|
||||||
|
),
|
||||||
|
agent(
|
||||||
|
'agent-creative-writer',
|
||||||
|
'Creative Writer',
|
||||||
|
'🎨',
|
||||||
|
'chat',
|
||||||
|
'Creative brainstorming and storytelling',
|
||||||
|
'You are a creative writer. Produce vivid, engaging output and offer multiple variants.',
|
||||||
|
'Let’s create something compelling together.',
|
||||||
|
['Write a short story', 'Brainstorm campaign ideas', 'Craft a product narrative']
|
||||||
|
),
|
||||||
|
agent(
|
||||||
|
'agent-rubber-duck',
|
||||||
|
'Rubber Duck',
|
||||||
|
'🦆',
|
||||||
|
'fast',
|
||||||
|
'Socratic debugging partner',
|
||||||
|
'You are a rubber duck. Ask short clarifying questions that help the user find the answer.',
|
||||||
|
'I will ask focused questions to help you reason through the issue.',
|
||||||
|
['Help me debug this bug', 'Ask me questions about this design', 'Unblock my implementation']
|
||||||
|
),
|
||||||
|
agent(
|
||||||
|
'agent-translator',
|
||||||
|
'Translator',
|
||||||
|
'🌐',
|
||||||
|
'chat',
|
||||||
|
'Accurate multilingual translator',
|
||||||
|
'You are a translator. Preserve meaning and tone with concise, natural phrasing.',
|
||||||
|
'I can translate and localize text with tone awareness.',
|
||||||
|
['Translate this to Spanish', 'Localize for UK English', 'Simplify this for global audience']
|
||||||
|
),
|
||||||
|
agent(
|
||||||
|
'agent-api-designer',
|
||||||
|
'API Designer',
|
||||||
|
'🔌',
|
||||||
|
'coding',
|
||||||
|
'REST/GraphQL API design expert',
|
||||||
|
'You are an API designer. Produce clean contracts, validation rules, and error models.',
|
||||||
|
'Tell me your API use case and I will design robust endpoints.',
|
||||||
|
['Design REST endpoints', 'Create OpenAPI schema', 'Design pagination strategy']
|
||||||
|
),
|
||||||
|
agent(
|
||||||
|
'agent-security-auditor',
|
||||||
|
'Security Auditor',
|
||||||
|
'🔒',
|
||||||
|
'reasoning',
|
||||||
|
'Application security reviewer',
|
||||||
|
'You are a security auditor. Identify vulnerabilities, severity, and practical remediations.',
|
||||||
|
'Share code or architecture and I will run a security review.',
|
||||||
|
['Review auth flow security', 'Find injection risks', 'Audit secret handling']
|
||||||
|
),
|
||||||
|
];
|
||||||
@ -9,6 +9,7 @@ import type {
|
|||||||
ScheduledTask,
|
ScheduledTask,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { BUILTIN_QUICK_ACTIONS } from './quick-actions';
|
import { BUILTIN_QUICK_ACTIONS } from './quick-actions';
|
||||||
|
import { BUILTIN_AGENTS } from './agents';
|
||||||
|
|
||||||
interface TaskRunRecord {
|
interface TaskRunRecord {
|
||||||
id?: number;
|
id?: number;
|
||||||
@ -315,3 +316,75 @@ export async function importQuickActions(actions: QuickAction[]): Promise<number
|
|||||||
}
|
}
|
||||||
return imported;
|
return imported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addAgent(agent: Agent): Promise<void> {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.put('agents', agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAgent(id: string): Promise<Agent | undefined> {
|
||||||
|
const db = await getDb();
|
||||||
|
return db.get('agents', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAgents(): Promise<Agent[]> {
|
||||||
|
const db = await getDb();
|
||||||
|
const rows = await db.getAll('agents');
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
if (a.builtin !== b.builtin) return a.builtin ? -1 : 1;
|
||||||
|
return b.conversationCount - a.conversationCount;
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAgent(id: string, partial: Partial<Agent>): Promise<Agent | null> {
|
||||||
|
const db = await getDb();
|
||||||
|
const existing = await db.get('agents', id);
|
||||||
|
if (!existing) return null;
|
||||||
|
const updated: Agent = { ...existing, ...partial, id };
|
||||||
|
await db.put('agents', updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAgent(id: string): Promise<void> {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.delete('agents', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedBuiltinAgents(): Promise<number> {
|
||||||
|
const seeded = typeof window !== 'undefined' ? localStorage.getItem('llm-agents-seeded') : 'true';
|
||||||
|
if (seeded === 'true') return 0;
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
|
const tx = db.transaction('agents', 'readwrite');
|
||||||
|
let inserted = 0;
|
||||||
|
for (const item of BUILTIN_AGENTS) {
|
||||||
|
const existing = await tx.store.get(item.id);
|
||||||
|
if (!existing) {
|
||||||
|
await tx.store.put(item);
|
||||||
|
inserted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await tx.done;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('llm-agents-seeded', 'true');
|
||||||
|
}
|
||||||
|
return inserted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportAgents(): Promise<Agent[]> {
|
||||||
|
const rows = await listAgents();
|
||||||
|
return rows.filter(row => !row.builtin);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importAgents(agents: Agent[]): Promise<number> {
|
||||||
|
const db = await getDb();
|
||||||
|
let imported = 0;
|
||||||
|
for (const agent of agents) {
|
||||||
|
const existing = await db.get('agents', agent.id);
|
||||||
|
if (existing) continue;
|
||||||
|
await db.put('agents', agent);
|
||||||
|
imported += 1;
|
||||||
|
}
|
||||||
|
return imported;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user