feat(audit): 审计修复第四轮 — 跨会话搜索、LLM压缩集成、Presentation渲染器
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- S9: MessageSearch 新增 Session/Global 双模式,Global 调用 VikingStorage memory_search - M4b: LLM 压缩器集成到 kernel AgentLoop,支持 use_llm 配置切换 - M4c: 压缩时自动提取记忆到 VikingStorage (runtime + tauri 双路径) - H6: 新增 ChartRenderer(recharts)、Document/Slideshow 完整渲染 - 累计修复 23 项,整体完成度 ~72%,真实可用率 ~80%
This commit is contained in:
@@ -1,19 +1,26 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Search, X, ChevronUp, ChevronDown, Clock, User, Filter } from 'lucide-react';
|
||||
import { Search, X, ChevronUp, ChevronDown, Clock, User, Filter, Globe, MessageSquare } from 'lucide-react';
|
||||
import { Button } from './ui';
|
||||
import { useChatStore, Message } from '../store/chatStore';
|
||||
import { intelligence, PersistentMemory } from '../lib/intelligence-backend';
|
||||
|
||||
export interface SearchFilters {
|
||||
sender: 'all' | 'user' | 'assistant';
|
||||
timeRange: 'all' | 'today' | 'week' | 'month';
|
||||
}
|
||||
|
||||
export type SearchScope = 'session' | 'global';
|
||||
|
||||
export interface SearchResult {
|
||||
message: Message;
|
||||
matchIndices: Array<{ start: number; end: number }>;
|
||||
}
|
||||
|
||||
export interface GlobalSearchResult {
|
||||
memory: PersistentMemory;
|
||||
}
|
||||
|
||||
interface MessageSearchProps {
|
||||
onNavigateToMessage: (messageId: string) => void;
|
||||
}
|
||||
@@ -26,6 +33,7 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
const [scope, setScope] = useState<SearchScope>('session');
|
||||
const [filters, setFilters] = useState<SearchFilters>({
|
||||
sender: 'all',
|
||||
timeRange: 'all',
|
||||
@@ -33,6 +41,8 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
|
||||
const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [searchHistory, setSearchHistory] = useState<string[]>([]);
|
||||
const [globalResults, setGlobalResults] = useState<GlobalSearchResult[]>([]);
|
||||
const [globalLoading, setGlobalLoading] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Load search history from localStorage
|
||||
@@ -63,6 +73,41 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Global search: query VikingStorage when scope is 'global'
|
||||
useEffect(() => {
|
||||
if (scope !== 'global' || !query.trim()) {
|
||||
setGlobalResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const debounceTimer = setTimeout(async () => {
|
||||
setGlobalLoading(true);
|
||||
try {
|
||||
const results = await intelligence.memory.search({
|
||||
query: query.trim(),
|
||||
limit: 20,
|
||||
});
|
||||
if (!cancelled) {
|
||||
setGlobalResults(results.map((memory) => ({ memory })));
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
setGlobalResults([]);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setGlobalLoading(false);
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(debounceTimer);
|
||||
};
|
||||
}, [scope, query]);
|
||||
|
||||
// Filter messages by time range
|
||||
const filterByTimeRange = useCallback((message: Message, timeRange: SearchFilters['timeRange']): boolean => {
|
||||
if (timeRange === 'all') return true;
|
||||
@@ -245,6 +290,36 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
|
||||
>
|
||||
<div className="px-4 py-3">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2">
|
||||
{/* Scope toggle */}
|
||||
<div className="flex items-center bg-gray-100 dark:bg-gray-700 rounded-lg p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setScope('session')}
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
|
||||
scope === 'session'
|
||||
? 'bg-white dark:bg-gray-600 text-orange-600 dark:text-orange-400 shadow-sm'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'
|
||||
}`}
|
||||
aria-label="Search current session"
|
||||
>
|
||||
<MessageSquare className="w-3 h-3" />
|
||||
<span className="hidden sm:inline">Session</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setScope('global')}
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
|
||||
scope === 'global'
|
||||
? 'bg-white dark:bg-gray-600 text-orange-600 dark:text-orange-400 shadow-sm'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'
|
||||
}`}
|
||||
aria-label="Search all memories"
|
||||
>
|
||||
<Globe className="w-3 h-3" />
|
||||
<span className="hidden sm:inline">Global</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search input */}
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
@@ -253,7 +328,7 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search messages..."
|
||||
placeholder={scope === 'global' ? 'Search all memories...' : 'Search messages...'}
|
||||
className="w-full pl-9 pr-8 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 dark:focus:ring-orange-400 focus:border-transparent"
|
||||
aria-label="Search query"
|
||||
/>
|
||||
@@ -269,22 +344,24 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filter toggle */}
|
||||
<Button
|
||||
type="button"
|
||||
variant={showFilters ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setShowFilters((prev) => !prev)}
|
||||
className="flex items-center gap-1"
|
||||
aria-label="Toggle filters"
|
||||
aria-expanded={showFilters}
|
||||
>
|
||||
<Filter className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Filters</span>
|
||||
</Button>
|
||||
{/* Filter toggle (session only) */}
|
||||
{scope === 'session' && (
|
||||
<Button
|
||||
type="button"
|
||||
variant={showFilters ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setShowFilters((prev) => !prev)}
|
||||
className="flex items-center gap-1"
|
||||
aria-label="Toggle filters"
|
||||
aria-expanded={showFilters}
|
||||
>
|
||||
<Filter className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Filters</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Navigation buttons */}
|
||||
{searchResults.length > 0 && (
|
||||
{/* Navigation buttons (session only) */}
|
||||
{scope === 'session' && searchResults.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 px-2">
|
||||
{currentMatchIndex + 1} / {searchResults.length}
|
||||
@@ -381,8 +458,58 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No results message */}
|
||||
{query && searchResults.length === 0 && (
|
||||
{/* Global search results */}
|
||||
{scope === 'global' && query && (
|
||||
<div className="mt-2 max-h-64 overflow-y-auto">
|
||||
{globalLoading && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 text-center py-2">
|
||||
Searching memories...
|
||||
</div>
|
||||
)}
|
||||
{!globalLoading && globalResults.length === 0 && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 text-center py-2">
|
||||
No memories found matching "{query}"
|
||||
</div>
|
||||
)}
|
||||
{!globalLoading && globalResults.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500 mb-1">
|
||||
{globalResults.length} memories found
|
||||
</div>
|
||||
{globalResults.map((result) => (
|
||||
<div
|
||||
key={result.memory.id}
|
||||
className="px-2 py-1.5 bg-white dark:bg-gray-700/50 border border-gray-100 dark:border-gray-600 rounded text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 mb-0.5">
|
||||
<span className="text-orange-500 dark:text-orange-400 font-medium">
|
||||
{result.memory.memory_type}
|
||||
</span>
|
||||
<span className="text-gray-300 dark:text-gray-600">|</span>
|
||||
<span className="text-gray-400 dark:text-gray-500">
|
||||
{result.memory.agent_id}
|
||||
</span>
|
||||
{result.memory.importance > 5 && (
|
||||
<span className="text-yellow-500">
|
||||
{'*'.repeat(Math.min(result.memory.importance - 4, 5))}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-gray-700 dark:text-gray-300 line-clamp-2">
|
||||
{highlightSearchMatches(result.memory.content, query)}
|
||||
</div>
|
||||
<div className="text-gray-400 dark:text-gray-500 mt-0.5">
|
||||
{result.memory.created_at.split('T')[0]}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No results message (session search) */}
|
||||
{scope === 'session' && query && searchResults.length === 0 && (
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400 text-center py-2">
|
||||
No messages found matching "{query}"
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user