diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9be233d3..8635841a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "@tailwindcss/vite": "^4.2.1", "@tauri-apps/api": "^2", "@tauri-apps/plugin-autostart": "^2", + "@tauri-apps/plugin-dialog": "^2.7.0", "@tauri-apps/plugin-global-shortcut": "^2", "@tauri-apps/plugin-notification": "^2", "@tauri-apps/plugin-process": "^2", @@ -3626,6 +3627,15 @@ "@tauri-apps/api": "^2.8.0" } }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.0.tgz", + "integrity": "sha512-4nS/hfGMGCXiAS3LtVjH9AgsSAPJeG/7R+q8agTFqytjnMa4Zq95Bq8WzVDkckpanX+yyRHXnRtrKXkANKDHvw==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, "node_modules/@tauri-apps/plugin-global-shortcut": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-global-shortcut/-/plugin-global-shortcut-2.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index be3dc612..7f2adce5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "@tailwindcss/vite": "^4.2.1", "@tauri-apps/api": "^2", "@tauri-apps/plugin-autostart": "^2", + "@tauri-apps/plugin-dialog": "^2.7.0", "@tauri-apps/plugin-global-shortcut": "^2", "@tauri-apps/plugin-notification": "^2", "@tauri-apps/plugin-process": "^2", diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 1068ef59..9c3438eb 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -17,6 +17,7 @@ tauri-plugin-autostart = "2" tauri-plugin-updater = "2" tauri-plugin-single-instance = "2" tauri-plugin-process = "2" +tauri-plugin-dialog = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" reqwest = { version = "0.12", features = ["json", "multipart"] } diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index e30b2460..502c7a22 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -18,6 +18,7 @@ "shell:allow-stdin-write", "shell:allow-kill", "shell:allow-open", + "dialog:allow-open", { "identifier": "shell:allow-execute", "allow": [ diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index a9926161..ef350902 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -1606,6 +1606,7 @@ pub fn run() { )) // .plugin(tauri_plugin_updater::Builder::new().build()) // disabled for local dev .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { if let Some(window) = app.get_webview_window("main") { let _ = window.set_focus(); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index ff7019c2..854b56d3 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -784,3 +784,69 @@ export async function submitSavings(data: SavingsSubmission): Promise { return false; } } + +// --------------------------------------------------------------------------- +// Memory +// --------------------------------------------------------------------------- + +export interface MemorySearchResult { + content: string; + score: number; + metadata: Record; +} + +export interface MemoryStats { + entries: number; + backend: string; + [key: string]: unknown; +} + +export interface MemoryConfig { + backend: string; + context_from_memory: boolean; + context_top_k: number; + context_min_score: number; + context_max_tokens: number; +} + +export async function getMemoryStats(): Promise { + const res = await fetch(`${getBase()}/v1/memory/stats`); + if (!res.ok) throw new Error('Failed to fetch memory stats'); + return res.json(); +} + +export async function searchMemory(query: string, topK: number = 5): Promise { + const res = await fetch(`${getBase()}/v1/memory/search`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, top_k: topK }), + }); + if (!res.ok) throw new Error('Failed to search memory'); + const data = await res.json(); + return data.results; +} + +export async function storeMemory(content: string, metadata?: Record): Promise { + const res = await fetch(`${getBase()}/v1/memory/store`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content, metadata }), + }); + if (!res.ok) throw new Error('Failed to store memory'); +} + +export async function indexMemoryPath(path: string): Promise<{ chunks_indexed: number }> { + const res = await fetch(`${getBase()}/v1/memory/index`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path }), + }); + if (!res.ok) throw new Error('Failed to index path'); + return res.json(); +} + +export async function getMemoryConfig(): Promise { + const res = await fetch(`${getBase()}/v1/memory/config`); + if (!res.ok) throw new Error('Failed to fetch memory config'); + return res.json(); +} diff --git a/frontend/src/pages/AgentsPage.tsx b/frontend/src/pages/AgentsPage.tsx index 9e1f56c5..c68848dd 100644 --- a/frontend/src/pages/AgentsPage.tsx +++ b/frontend/src/pages/AgentsPage.tsx @@ -1453,8 +1453,8 @@ function InteractTab({ agentId, agentStatus }: { agentId: string; agentStatus: s .reverse(); return ( -
-
+
+
{displayMessages.length === 0 && !waitingForResponse && (
No messages yet. Send a message to interact with this agent. diff --git a/frontend/src/pages/DataSourcesPage.tsx b/frontend/src/pages/DataSourcesPage.tsx index 37043b6f..f2e9de4f 100644 --- a/frontend/src/pages/DataSourcesPage.tsx +++ b/frontend/src/pages/DataSourcesPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState, useCallback, useRef } from 'react'; import { useAppStore } from '../lib/store'; import { fetchManagedAgents, @@ -8,10 +8,14 @@ import { createManagedAgent, sendblueRegisterWebhook, sendblueHealth, + getMemoryStats, + searchMemory, + storeMemory, + indexMemoryPath, } from '../lib/api'; -import type { ChannelBinding, ManagedAgent } from '../lib/api'; -import { getBase } from '../lib/api'; -import { Database, MessageSquare, Loader2 } from 'lucide-react'; +import type { ChannelBinding, ManagedAgent, MemoryStats, MemorySearchResult } from '../lib/api'; +import { getBase, isTauri } from '../lib/api'; +import { Database, MessageSquare, Loader2, Brain, Search, FolderOpen, FileText } from 'lucide-react'; import { SOURCE_CATALOG } from '../types/connectors'; import type { ConnectRequest } from '../types/connectors'; import { listConnectors, connectSource, getSyncStatus, triggerSync } from '../lib/connectors-api'; @@ -1411,13 +1415,378 @@ function MessagingSection({ agentId }: { agentId: string }) { ); } +// --------------------------------------------------------------------------- +// Memory section +// --------------------------------------------------------------------------- + +function MemorySection() { + const [stats, setStats] = useState(null); + const [statsError, setStatsError] = useState(''); + + // Search + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [searching, setSearching] = useState(false); + const [searchDone, setSearchDone] = useState(false); + + // Index + const [indexPath, setIndexPath] = useState(''); + const [indexing, setIndexing] = useState(false); + const [indexResult, setIndexResult] = useState(''); + const [indexError, setIndexError] = useState(''); + + // Store + const [storeContent, setStoreContent] = useState(''); + const [storing, setStoring] = useState(false); + const [storeResult, setStoreResult] = useState(''); + const [storeError, setStoreError] = useState(''); + + const statsInterval = useRef | null>(null); + + const loadStats = useCallback(() => { + getMemoryStats() + .then((s) => { setStats(s); setStatsError(''); }) + .catch(() => setStatsError('Could not reach memory backend')); + }, []); + + useEffect(() => { + loadStats(); + statsInterval.current = setInterval(loadStats, 10000); + return () => { if (statsInterval.current) clearInterval(statsInterval.current); }; + }, [loadStats]); + + const handleSearch = async () => { + if (!searchQuery.trim()) return; + setSearching(true); + setSearchDone(false); + try { + const results = await searchMemory(searchQuery.trim()); + setSearchResults(results || []); + setSearchDone(true); + } catch { + setSearchResults([]); + setSearchDone(true); + } finally { + setSearching(false); + } + }; + + const handleBrowse = async () => { + if (isTauri()) { + try { + const { open } = await import('@tauri-apps/plugin-dialog'); + const selected = await open({ directory: true, multiple: false, title: 'Select folder to index' }); + if (selected) setIndexPath(selected as string); + return; + } catch { + // fall through to browser picker + } + } + const input = document.createElement('input'); + input.type = 'file'; + input.setAttribute('webkitdirectory', ''); + input.onchange = () => { + const files = input.files; + if (files && files.length > 0) { + const rel = (files[0] as any).webkitRelativePath || ''; + const folder = rel.split('/')[0]; + if (folder) setIndexPath(folder); + } + }; + input.click(); + }; + + const handleIndex = async () => { + if (!indexPath.trim()) return; + setIndexing(true); + setIndexResult(''); + setIndexError(''); + try { + const res = await indexMemoryPath(indexPath.trim()); + setIndexResult(`Indexed ${res.chunks_indexed} chunk${res.chunks_indexed !== 1 ? 's' : ''}`); + setIndexPath(''); + loadStats(); + } catch (err: any) { + setIndexError(err.message || 'Indexing failed'); + } finally { + setIndexing(false); + } + }; + + const handleStore = async () => { + if (!storeContent.trim()) return; + setStoring(true); + setStoreResult(''); + setStoreError(''); + try { + await storeMemory(storeContent.trim()); + setStoreResult('Stored successfully'); + setStoreContent(''); + loadStats(); + } catch (err: any) { + setStoreError(err.message || 'Failed to store'); + } finally { + setStoring(false); + } + }; + + return ( +
+ {/* Stats overview */} +
+ {/* Subtle gradient accent along top edge */} +
+
+
+
+ +
+
+

Memory Backend

+ {statsError ? ( +

{statsError}

+ ) : stats ? ( +
+ 0 ? 'var(--color-success)' : 'var(--color-text-tertiary)', + }} /> + + {stats.backend} · {stats.entries.toLocaleString()} {stats.entries === 1 ? 'chunk' : 'chunks'} + +
+ ) : ( +

Connecting...

+ )} +
+
+ {stats && stats.entries > 0 && ( +
+
+ {stats.entries.toLocaleString()} +
+
+ indexed +
+
+ )} +
+
+ + {/* Search */} +
+
+ +

Search Memory

+
+
+
+ setSearchQuery(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') handleSearch(); }} + placeholder="What are you looking for?" + className="w-full text-sm px-3 py-2 rounded-lg outline-none transition-colors" + style={{ + background: 'var(--color-bg)', + border: '1px solid var(--color-border)', + color: 'var(--color-text)', + }} + /> +
+ +
+ + {/* Results */} + {searchDone && searchResults.length === 0 && ( +
+ +

No matching memories found

+
+ )} + {searchResults.length > 0 && ( +
+ {searchResults.map((r, i) => ( +
+

+ {r.content.length > 250 ? r.content.slice(0, 250) + '...' : r.content} +

+
+ 0.5 + ? 'rgba(74, 222, 128, 0.1)' + : r.score > 0.2 + ? 'var(--color-accent-amber-subtle)' + : 'var(--color-bg-tertiary)', + color: r.score > 0.5 + ? 'var(--color-success)' + : r.score > 0.2 + ? 'var(--color-warning)' + : 'var(--color-text-tertiary)', + }}> + {(r.score * 100).toFixed(0)}% match + + {r.metadata?.source != null && ( + + {String(r.metadata.source)} + + )} +
+
+ ))} +
+ )} +
+ + {/* Add to Memory — two-column grid */} +
+ {/* Index folder */} +
+
+ +

Index Folder

+
+

+ Scan a folder and index all supported files into memory. +

+
+ setIndexPath(e.target.value)} + placeholder="~/Documents/notes" + className="flex-1 text-sm px-3 py-2 rounded-lg outline-none" + style={{ + background: 'var(--color-bg)', + border: '1px solid var(--color-border)', + color: 'var(--color-text)', + }} + /> + {isTauri() && ( + + )} +
+ + {indexResult && ( +

{indexResult}

+ )} + {indexError && ( +

{indexError}

+ )} +
+ + {/* Paste content */} +
+
+ +

Store Text

+
+

+ Paste any text to add directly to your memory store. +

+