fix(ui): 5 项 E2E 测试 Bug 修复 — Agent 502 / 错误持久化 / 模型标记 / 侧面板 / 记忆页
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
- BUG-01: createFromTemplate 在 saas-relay 模式下 try-catch 跳过本地 Kernel - BUG-02: upsertActiveConversation 持久化前剥离 error/streaming/optimistic 字段 - BUG-04: ModelSelector 添加 available 标记,ChatArea 追踪失败模型 ID - BUG-05: VikingPanel 移除 status?.available 门控,不可用时 disabled + 重连按钮 - BUG-06: 侧面板 tooltip 改为"查看产物文件",空状态增加图标和说明
This commit is contained in:
@@ -196,68 +196,89 @@ export function VikingPanel() {
|
||||
)}
|
||||
|
||||
{/* Storage Info */}
|
||||
{status?.available && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-500 flex items-center justify-center">
|
||||
<Database className="w-4 h-4 text-white" />
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${status?.available ? 'bg-gradient-to-br from-blue-500 to-indigo-500' : 'bg-gray-300 dark:bg-gray-600'}`}>
|
||||
<Database className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
本地存储
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
本地存储
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{status.version || 'Native'} · {status.dataDir || '默认路径'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{status?.available
|
||||
? `${status.version || 'Native'} · ${status.dataDir || '默认路径'}`
|
||||
: '存储未连接'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4 text-xs">
|
||||
<div className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
|
||||
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
||||
<span>SQLite + FTS5</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
|
||||
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
||||
<span>TF-IDF 语义评分</span>
|
||||
</div>
|
||||
{memoryCount !== null && (
|
||||
<div className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
|
||||
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
||||
<span>{memoryCount} 条记忆</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!status?.available && (
|
||||
<button
|
||||
onClick={loadStatus}
|
||||
disabled={isLoading}
|
||||
className="ml-auto text-xs text-amber-600 dark:text-amber-400 hover:text-amber-700 dark:hover:text-amber-300 flex items-center gap-1 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-3 h-3 ${isLoading ? 'animate-spin' : ''}`} /> 重新连接
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-4 text-xs">
|
||||
<div className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
|
||||
{status?.available ? (
|
||||
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
||||
) : (
|
||||
<AlertCircle className="w-3.5 h-3.5 text-amber-500" />
|
||||
)}
|
||||
<span>SQLite + FTS5</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
|
||||
{status?.available ? (
|
||||
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
||||
) : (
|
||||
<AlertCircle className="w-3.5 h-3.5 text-amber-500" />
|
||||
)}
|
||||
<span>TF-IDF 语义评分</span>
|
||||
</div>
|
||||
{memoryCount !== null && (
|
||||
<div className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
|
||||
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
||||
<span>{memoryCount} 条记忆</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Box */}
|
||||
{status?.available && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3">语义搜索</h3>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="输入自然语言查询..."
|
||||
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={isSearching || !searchQuery.trim()}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 flex items-center gap-2 text-sm"
|
||||
>
|
||||
{isSearching ? (
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Search className="w-4 h-4" />
|
||||
)}
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3">语义搜索</h3>
|
||||
{!status?.available && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400 mb-2 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" /> 存储未连接,搜索功能不可用
|
||||
</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="输入自然语言查询..."
|
||||
disabled={!status?.available}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={isSearching || !searchQuery.trim() || !status?.available}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 flex items-center gap-2 text-sm"
|
||||
>
|
||||
{isSearching ? (
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Search className="w-4 h-4" />
|
||||
)}
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Results */}
|
||||
{searchResults.length > 0 && (
|
||||
@@ -385,59 +406,64 @@ export function VikingPanel() {
|
||||
)}
|
||||
|
||||
{/* Summary Generation */}
|
||||
{status?.available && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3">智能摘要</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
存储资源并自动通过 LLM 生成 L0/L1 多级摘要(需配置摘要驱动)
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3">智能摘要</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
存储资源并自动通过 LLM 生成 L0/L1 多级摘要(需配置摘要驱动)
|
||||
</p>
|
||||
{!status?.available && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400 mb-2 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" /> 存储未连接,摘要功能不可用
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={summaryUri}
|
||||
onChange={(e) => setSummaryUri(e.target.value)}
|
||||
placeholder="资源 URI (如: notes/project-plan)"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<textarea
|
||||
value={summaryContent}
|
||||
onChange={(e) => setSummaryContent(e.target.value)}
|
||||
placeholder="资源内容..."
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||
/>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!summaryUri.trim() || !summaryContent.trim()) return;
|
||||
setIsGeneratingSummary(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
await storeWithSummaries(summaryUri, summaryContent);
|
||||
setMessage({ type: 'success', text: `摘要生成完成: ${summaryUri}` });
|
||||
setSummaryUri('');
|
||||
setSummaryContent('');
|
||||
} catch (error) {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: `摘要生成失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
});
|
||||
} finally {
|
||||
setIsGeneratingSummary(false);
|
||||
}
|
||||
}}
|
||||
disabled={isGeneratingSummary || !summaryUri.trim() || !summaryContent.trim()}
|
||||
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 flex items-center gap-2 text-sm"
|
||||
>
|
||||
{isGeneratingSummary ? (
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="w-4 h-4" />
|
||||
)}
|
||||
生成摘要并存储
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={summaryUri}
|
||||
onChange={(e) => setSummaryUri(e.target.value)}
|
||||
placeholder="资源 URI (如: notes/project-plan)"
|
||||
disabled={!status?.available}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<textarea
|
||||
value={summaryContent}
|
||||
onChange={(e) => setSummaryContent(e.target.value)}
|
||||
placeholder="资源内容..."
|
||||
rows={3}
|
||||
disabled={!status?.available}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!summaryUri.trim() || !summaryContent.trim()) return;
|
||||
setIsGeneratingSummary(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
await storeWithSummaries(summaryUri, summaryContent);
|
||||
setMessage({ type: 'success', text: `摘要生成完成: ${summaryUri}` });
|
||||
setSummaryUri('');
|
||||
setSummaryContent('');
|
||||
} catch (error) {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: `摘要生成失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
});
|
||||
} finally {
|
||||
setIsGeneratingSummary(false);
|
||||
}
|
||||
}}
|
||||
disabled={isGeneratingSummary || !summaryUri.trim() || !summaryContent.trim() || !status?.available}
|
||||
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 flex items-center gap-2 text-sm"
|
||||
>
|
||||
{isGeneratingSummary ? (
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="w-4 h-4" />
|
||||
)}
|
||||
生成摘要并存储
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="mt-6 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
|
||||
Reference in New Issue
Block a user