Add AI prompt tool for natural language graph operations
New MCP tool and portal UI for executing natural language instructions against the memory graph via Ollama (qwen3-coder:30b). Single LLM call generates a JSON action plan which is executed sequentially. Supports 8 action types: add_node, update_node, remove_node, add_edge, remove_edge, bulk_tag, reorganize, query. Actions can reference previous results via $result[N].field interpolation. Uses /api/chat with few-shot assistant example, format:json, and temperature:0 for reliable output.
This commit is contained in:
137
portal/src/components/PromptPanel.tsx
Normal file
137
portal/src/components/PromptPanel.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useState } from 'react';
|
||||
import { api } from '../api';
|
||||
import type { PromptResult } from '../types';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onDone: () => void;
|
||||
}
|
||||
|
||||
export default function PromptPanel({ onClose, onDone }: Props) {
|
||||
const [text, setText] = useState('');
|
||||
const [result, setResult] = useState<PromptResult | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const submit = async () => {
|
||||
if (!text.trim()) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
try {
|
||||
const data = await api.prompt(text);
|
||||
setResult(data);
|
||||
if (data.success) onDone();
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Request failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-gray-900/95 backdrop-blur border-t border-amber-500/30 rounded-t-xl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700/50">
|
||||
<span className="text-amber-300 font-medium text-sm">AI Prompt</span>
|
||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-300 text-lg">×</button>
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="px-4 py-3 flex gap-2">
|
||||
<input
|
||||
value={text}
|
||||
onChange={e => setText(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && !loading && submit()}
|
||||
placeholder='e.g. "create a decision node about using Redis for caching"'
|
||||
className="flex-1 bg-gray-800 border border-gray-600 rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:outline-none focus:border-amber-500"
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
onClick={submit}
|
||||
disabled={loading || !text.trim()}
|
||||
className="px-4 py-2 bg-amber-600 hover:bg-amber-500 disabled:opacity-50 rounded-lg text-sm text-white font-medium min-w-[80px]"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="inline-block animate-pulse">Running...</span>
|
||||
) : 'Run'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
<div className="flex-1 overflow-y-auto px-4 pb-4">
|
||||
{!result && !loading && !error && (
|
||||
<div className="text-gray-500 text-sm mt-4 space-y-1">
|
||||
<p>Give a natural language instruction to modify the graph:</p>
|
||||
<p className="text-gray-600 ml-2">"create a decision node about using Redis for caching"</p>
|
||||
<p className="text-gray-600 ml-2">"tag all task nodes with 'backlog'"</p>
|
||||
<p className="text-gray-600 ml-2">"create a component for auth and link it to the API gateway"</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="text-amber-400/70 text-sm mt-4 animate-pulse">
|
||||
Generating and executing action plan...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 p-3 rounded-lg bg-red-900/30 border border-red-700/50 text-red-300 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div className="space-y-3 mt-2">
|
||||
{/* Summary */}
|
||||
<div className={`p-3 rounded-lg border text-sm ${
|
||||
result.success
|
||||
? 'bg-green-900/20 border-green-700/50 text-green-300'
|
||||
: 'bg-yellow-900/20 border-yellow-700/50 text-yellow-300'
|
||||
}`}>
|
||||
<div className="font-medium mb-1">{result.success ? 'Completed' : 'Completed with errors'}</div>
|
||||
<div>{result.summary}</div>
|
||||
</div>
|
||||
|
||||
{/* Reasoning */}
|
||||
{result.reasoning && (
|
||||
<div className="text-xs text-gray-400 bg-gray-800/50 rounded-lg p-3">
|
||||
<span className="text-gray-500 font-medium">Reasoning: </span>
|
||||
{result.reasoning}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Execution log */}
|
||||
{result.executionLog.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-gray-500 font-medium uppercase tracking-wide">Execution Log</div>
|
||||
{result.executionLog.map((entry, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex items-start gap-2 p-2 rounded text-sm ${
|
||||
entry.status === 'completed' ? 'bg-gray-800/30' : 'bg-red-900/20'
|
||||
}`}
|
||||
>
|
||||
<span className={`mt-0.5 text-xs ${
|
||||
entry.status === 'completed' ? 'text-green-500' : 'text-red-500'
|
||||
}`}>
|
||||
{entry.status === 'completed' ? '✓' : '✗'}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-gray-300 truncate">{entry.description}</div>
|
||||
<div className="text-xs text-gray-500">{entry.action}</div>
|
||||
{entry.error && (
|
||||
<div className="text-xs text-red-400 mt-1">{entry.error}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user