@geminilight/mindos 0.5.22 → 0.5.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/app/api/ask/route.ts +7 -14
- package/app/app/api/bootstrap/route.ts +1 -0
- package/app/app/globals.css +14 -0
- package/app/app/setup/page.tsx +3 -2
- package/app/components/ActivityBar.tsx +183 -0
- package/app/components/AskFab.tsx +39 -97
- package/app/components/AskModal.tsx +13 -371
- package/app/components/Breadcrumb.tsx +4 -4
- package/app/components/FileTree.tsx +21 -4
- package/app/components/Logo.tsx +39 -0
- package/app/components/Panel.tsx +152 -0
- package/app/components/RightAskPanel.tsx +72 -0
- package/app/components/SettingsModal.tsx +9 -241
- package/app/components/SidebarLayout.tsx +426 -12
- package/app/components/SyncStatusBar.tsx +74 -53
- package/app/components/TableOfContents.tsx +4 -2
- package/app/components/ask/AskContent.tsx +418 -0
- package/app/components/ask/MessageList.tsx +2 -2
- package/app/components/panels/AgentsPanel.tsx +231 -0
- package/app/components/panels/PanelHeader.tsx +35 -0
- package/app/components/panels/PluginsPanel.tsx +106 -0
- package/app/components/panels/SearchPanel.tsx +178 -0
- package/app/components/panels/SyncPopover.tsx +105 -0
- package/app/components/renderers/csv/TableView.tsx +4 -4
- package/app/components/settings/AiTab.tsx +39 -1
- package/app/components/settings/KnowledgeTab.tsx +116 -2
- package/app/components/settings/McpTab.tsx +6 -6
- package/app/components/settings/SettingsContent.tsx +343 -0
- package/app/components/settings/types.ts +1 -1
- package/app/components/setup/index.tsx +2 -23
- package/app/hooks/useResizeDrag.ts +78 -0
- package/app/lib/agent/index.ts +0 -1
- package/app/lib/agent/model.ts +33 -10
- package/app/lib/format.ts +19 -0
- package/app/lib/i18n-en.ts +6 -6
- package/app/lib/i18n-zh.ts +5 -5
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +1 -1
- package/app/package.json +2 -2
- package/bin/cli.js +27 -97
- package/package.json +4 -2
- package/scripts/setup.js +2 -12
- package/skills/mindos/SKILL.md +226 -8
- package/skills/mindos-zh/SKILL.md +226 -8
- package/app/lib/agent/skill-rules.ts +0 -70
- package/app/package-lock.json +0 -15736
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Maximize2, Minimize2 } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared header bar for side panels (Files, Search, Plugins, etc.)
|
|
5
|
+
* Keeps the uppercase label + optional right content pattern consistent.
|
|
6
|
+
*/
|
|
7
|
+
export default function PanelHeader({
|
|
8
|
+
title,
|
|
9
|
+
children,
|
|
10
|
+
maximized,
|
|
11
|
+
onMaximize,
|
|
12
|
+
}: {
|
|
13
|
+
title: string;
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
maximized?: boolean;
|
|
16
|
+
onMaximize?: () => void;
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
|
|
20
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider font-display">{title}</span>
|
|
21
|
+
<div className="flex items-center gap-1">
|
|
22
|
+
{children}
|
|
23
|
+
{onMaximize && (
|
|
24
|
+
<button
|
|
25
|
+
onClick={onMaximize}
|
|
26
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
27
|
+
aria-label={maximized ? 'Restore panel' : 'Maximize panel'}
|
|
28
|
+
>
|
|
29
|
+
{maximized ? <Minimize2 size={13} /> : <Maximize2 size={13} />}
|
|
30
|
+
</button>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { getAllRenderers, isRendererEnabled, setRendererEnabled, loadDisabledState } from '@/lib/renderers/registry';
|
|
6
|
+
import { Toggle } from '../settings/Primitives';
|
|
7
|
+
import PanelHeader from './PanelHeader';
|
|
8
|
+
|
|
9
|
+
interface PluginsPanelProps {
|
|
10
|
+
active: boolean;
|
|
11
|
+
maximized?: boolean;
|
|
12
|
+
onMaximize?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function PluginsPanel({ active, maximized, onMaximize }: PluginsPanelProps) {
|
|
16
|
+
const [mounted, setMounted] = useState(false);
|
|
17
|
+
const [, forceUpdate] = useState(0);
|
|
18
|
+
const router = useRouter();
|
|
19
|
+
|
|
20
|
+
// Defer renderer reads to client only — avoids hydration mismatch
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
loadDisabledState();
|
|
23
|
+
setMounted(true);
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const renderers = mounted ? getAllRenderers() : [];
|
|
27
|
+
const enabledCount = mounted ? renderers.filter(r => isRendererEnabled(r.id)).length : 0;
|
|
28
|
+
|
|
29
|
+
const handleToggle = (id: string, enabled: boolean) => {
|
|
30
|
+
setRendererEnabled(id, enabled);
|
|
31
|
+
forceUpdate(n => n + 1);
|
|
32
|
+
window.dispatchEvent(new Event('renderer-state-changed'));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
37
|
+
{/* Header */}
|
|
38
|
+
<PanelHeader title="Plugins" maximized={maximized} onMaximize={onMaximize}>
|
|
39
|
+
<span className="text-2xs text-muted-foreground">{enabledCount}/{renderers.length} active</span>
|
|
40
|
+
</PanelHeader>
|
|
41
|
+
|
|
42
|
+
{/* Plugin list */}
|
|
43
|
+
<div className="flex-1 overflow-y-auto min-h-0">
|
|
44
|
+
{mounted && renderers.length === 0 && (
|
|
45
|
+
<p className="px-4 py-8 text-sm text-muted-foreground text-center">No plugins registered</p>
|
|
46
|
+
)}
|
|
47
|
+
{renderers.map(r => {
|
|
48
|
+
const enabled = isRendererEnabled(r.id);
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
key={r.id}
|
|
52
|
+
className="px-4 py-3 border-b border-border/50 hover:bg-muted/30 transition-colors"
|
|
53
|
+
>
|
|
54
|
+
{/* Top row: icon + name + toggle */}
|
|
55
|
+
<div className="flex items-center justify-between gap-2">
|
|
56
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
57
|
+
<span className="text-base shrink-0">{r.icon}</span>
|
|
58
|
+
<span className="text-sm font-medium text-foreground truncate">{r.name}</span>
|
|
59
|
+
{r.core && (
|
|
60
|
+
<span className="text-2xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground shrink-0">Core</span>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
<Toggle
|
|
64
|
+
checked={enabled}
|
|
65
|
+
onChange={(v) => handleToggle(r.id, v)}
|
|
66
|
+
size="sm"
|
|
67
|
+
disabled={r.core}
|
|
68
|
+
title={r.core ? 'Core plugin — cannot be disabled' : undefined}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* Description */}
|
|
73
|
+
<p className="mt-1 text-xs text-muted-foreground leading-relaxed pl-[30px]">
|
|
74
|
+
{r.description}
|
|
75
|
+
</p>
|
|
76
|
+
|
|
77
|
+
{/* Tags + entry path */}
|
|
78
|
+
<div className="mt-1.5 flex items-center gap-1.5 pl-[30px] flex-wrap">
|
|
79
|
+
{r.tags.slice(0, 3).map(tag => (
|
|
80
|
+
<span key={tag} className="text-2xs px-1.5 py-0.5 rounded-full bg-muted/60 text-muted-foreground">
|
|
81
|
+
{tag}
|
|
82
|
+
</span>
|
|
83
|
+
))}
|
|
84
|
+
{r.entryPath && enabled && (
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => router.push(`/view/${r.entryPath!.split('/').map(encodeURIComponent).join('/')}`)}
|
|
87
|
+
className="text-2xs px-1.5 py-0.5 rounded-full text-[var(--amber)] hover:bg-[var(--amber-dim)] transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
|
88
|
+
>
|
|
89
|
+
→ {r.entryPath}
|
|
90
|
+
</button>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
})}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Footer info */}
|
|
99
|
+
<div className="px-4 py-2 border-t border-border shrink-0">
|
|
100
|
+
<p className="text-2xs text-muted-foreground">
|
|
101
|
+
Plugins customize how files render. Core plugins cannot be disabled.
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { Search, X, FileText, Table } from 'lucide-react';
|
|
6
|
+
import { SearchResult } from '@/lib/types';
|
|
7
|
+
import { encodePath } from '@/lib/utils';
|
|
8
|
+
import { apiFetch } from '@/lib/api';
|
|
9
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
10
|
+
import PanelHeader from './PanelHeader';
|
|
11
|
+
|
|
12
|
+
/** Highlight matched text fragments in a snippet based on the query */
|
|
13
|
+
function highlightSnippet(snippet: string, query: string): React.ReactNode {
|
|
14
|
+
if (!query.trim()) return snippet;
|
|
15
|
+
const words = query.trim().split(/\s+/).filter(Boolean);
|
|
16
|
+
const escaped = words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
17
|
+
const pattern = new RegExp(`(${escaped.join('|')})`, 'gi');
|
|
18
|
+
const parts = snippet.split(pattern);
|
|
19
|
+
return parts.map((part, i) =>
|
|
20
|
+
pattern.test(part) ? <mark key={i} className="bg-yellow-300/40 text-foreground rounded-sm px-0.5">{part}</mark> : part
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SearchPanelProps {
|
|
25
|
+
/** When true the panel is visible — triggers focus & reset */
|
|
26
|
+
active: boolean;
|
|
27
|
+
/** Called when user navigates to a result (panel host may want to close) */
|
|
28
|
+
onNavigate?: () => void;
|
|
29
|
+
maximized?: boolean;
|
|
30
|
+
onMaximize?: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default function SearchPanel({ active, onNavigate, maximized, onMaximize }: SearchPanelProps) {
|
|
34
|
+
const [query, setQuery] = useState('');
|
|
35
|
+
const [results, setResults] = useState<SearchResult[]>([]);
|
|
36
|
+
const [loading, setLoading] = useState(false);
|
|
37
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
38
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
39
|
+
const router = useRouter();
|
|
40
|
+
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
41
|
+
const { t } = useLocale();
|
|
42
|
+
|
|
43
|
+
// Focus input when panel becomes active
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (active) {
|
|
46
|
+
setTimeout(() => inputRef.current?.focus(), 50);
|
|
47
|
+
}
|
|
48
|
+
}, [active]);
|
|
49
|
+
|
|
50
|
+
// Debounced search
|
|
51
|
+
const doSearch = useCallback((q: string) => {
|
|
52
|
+
if (debounceTimer.current) clearTimeout(debounceTimer.current);
|
|
53
|
+
if (!q.trim()) {
|
|
54
|
+
setResults([]);
|
|
55
|
+
setLoading(false);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
setLoading(true);
|
|
59
|
+
debounceTimer.current = setTimeout(async () => {
|
|
60
|
+
try {
|
|
61
|
+
const data = await apiFetch<SearchResult[]>(`/api/search?q=${encodeURIComponent(q)}`);
|
|
62
|
+
setResults(Array.isArray(data) ? data : []);
|
|
63
|
+
setSelectedIndex(0);
|
|
64
|
+
} catch {
|
|
65
|
+
setResults([]);
|
|
66
|
+
} finally {
|
|
67
|
+
setLoading(false);
|
|
68
|
+
}
|
|
69
|
+
}, 300);
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
73
|
+
const val = e.target.value;
|
|
74
|
+
setQuery(val);
|
|
75
|
+
doSearch(val);
|
|
76
|
+
}, [doSearch]);
|
|
77
|
+
|
|
78
|
+
const navigate = useCallback((result: SearchResult) => {
|
|
79
|
+
router.push(`/view/${encodePath(result.path)}`);
|
|
80
|
+
onNavigate?.();
|
|
81
|
+
}, [router, onNavigate]);
|
|
82
|
+
|
|
83
|
+
// Keyboard navigation within the panel
|
|
84
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
85
|
+
if (e.key === 'ArrowDown') {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
setSelectedIndex(i => Math.min(i + 1, results.length - 1));
|
|
88
|
+
} else if (e.key === 'ArrowUp') {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
setSelectedIndex(i => Math.max(i - 1, 0));
|
|
91
|
+
} else if (e.key === 'Enter') {
|
|
92
|
+
if (results[selectedIndex]) navigate(results[selectedIndex]);
|
|
93
|
+
}
|
|
94
|
+
}, [results, selectedIndex, navigate]);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
98
|
+
{/* Header */}
|
|
99
|
+
<PanelHeader title="Search" maximized={maximized} onMaximize={onMaximize} />
|
|
100
|
+
|
|
101
|
+
{/* Search input */}
|
|
102
|
+
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-border shrink-0">
|
|
103
|
+
<Search size={14} className="text-muted-foreground shrink-0" />
|
|
104
|
+
<input
|
|
105
|
+
ref={inputRef}
|
|
106
|
+
type="text"
|
|
107
|
+
value={query}
|
|
108
|
+
onChange={handleChange}
|
|
109
|
+
onKeyDown={handleKeyDown}
|
|
110
|
+
placeholder={t.search.placeholder}
|
|
111
|
+
className="flex-1 bg-transparent text-foreground placeholder:text-muted-foreground text-sm outline-none"
|
|
112
|
+
/>
|
|
113
|
+
{loading && (
|
|
114
|
+
<div className="w-3.5 h-3.5 border-2 border-muted-foreground/40 border-t-foreground rounded-full animate-spin shrink-0" />
|
|
115
|
+
)}
|
|
116
|
+
{!loading && query && (
|
|
117
|
+
<button onClick={() => { setQuery(''); setResults([]); inputRef.current?.focus(); }}>
|
|
118
|
+
<X size={13} className="text-muted-foreground hover:text-foreground" />
|
|
119
|
+
</button>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Results */}
|
|
124
|
+
<div className="flex-1 overflow-y-auto min-h-0">
|
|
125
|
+
{results.length === 0 && query && !loading && (
|
|
126
|
+
<div className="px-4 py-8 text-center text-sm text-muted-foreground">{t.search.noResults}</div>
|
|
127
|
+
)}
|
|
128
|
+
{results.length === 0 && !query && (
|
|
129
|
+
<div className="px-4 py-8 text-center text-sm text-muted-foreground/60">{t.search.prompt}</div>
|
|
130
|
+
)}
|
|
131
|
+
{results.map((result, i) => {
|
|
132
|
+
const ext = result.path.endsWith('.csv') ? '.csv' : '.md';
|
|
133
|
+
const parts = result.path.split('/');
|
|
134
|
+
const fileName = parts[parts.length - 1];
|
|
135
|
+
const dirPath = parts.slice(0, -1).join('/');
|
|
136
|
+
return (
|
|
137
|
+
<button
|
|
138
|
+
key={result.path}
|
|
139
|
+
onClick={() => navigate(result)}
|
|
140
|
+
onMouseEnter={() => setSelectedIndex(i)}
|
|
141
|
+
className={`
|
|
142
|
+
w-full px-4 py-2.5 flex items-start gap-3 text-left transition-colors duration-75
|
|
143
|
+
${i === selectedIndex ? 'bg-muted' : 'hover:bg-muted/50'}
|
|
144
|
+
${i < results.length - 1 ? 'border-b border-border' : ''}
|
|
145
|
+
`}
|
|
146
|
+
>
|
|
147
|
+
{ext === '.csv'
|
|
148
|
+
? <Table size={13} className="text-success shrink-0 mt-0.5" />
|
|
149
|
+
: <FileText size={13} className="text-muted-foreground shrink-0 mt-0.5" />
|
|
150
|
+
}
|
|
151
|
+
<div className="min-w-0 flex-1">
|
|
152
|
+
<div className="flex items-baseline gap-2 flex-wrap">
|
|
153
|
+
<span className="text-sm text-foreground font-medium truncate">{fileName}</span>
|
|
154
|
+
{dirPath && (
|
|
155
|
+
<span className="text-xs text-muted-foreground truncate">{dirPath}</span>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
{result.snippet && (
|
|
159
|
+
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed">
|
|
160
|
+
{highlightSnippet(result.snippet, query)}
|
|
161
|
+
</p>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
</button>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* Footer hints */}
|
|
170
|
+
{results.length > 0 && (
|
|
171
|
+
<div className="px-4 py-2 border-t border-border flex items-center gap-3 text-xs text-muted-foreground/50 shrink-0">
|
|
172
|
+
<span><kbd className="font-mono">↑↓</kbd> {t.search.navigate}</span>
|
|
173
|
+
<span><kbd className="font-mono">↵</kbd> {t.search.open}</span>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { RefreshCw, CheckCircle2, XCircle, X } from 'lucide-react';
|
|
5
|
+
import { DOT_COLORS, getStatusLevel, getSyncLabel, useSyncAction } from '../SyncStatusBar';
|
|
6
|
+
import type { SyncStatus } from '../settings/SyncTab';
|
|
7
|
+
import { PrimaryButton } from '../settings/Primitives';
|
|
8
|
+
|
|
9
|
+
interface SyncPopoverProps {
|
|
10
|
+
open: boolean;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
anchorRect: DOMRect | null;
|
|
13
|
+
railWidth: number;
|
|
14
|
+
onOpenSyncSettings: () => void;
|
|
15
|
+
syncStatus: SyncStatus | null;
|
|
16
|
+
onSyncStatusRefresh: () => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function SyncPopover({ open, onClose, anchorRect, railWidth, onOpenSyncSettings, syncStatus, onSyncStatusRefresh }: SyncPopoverProps) {
|
|
20
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
21
|
+
const onCloseRef = useRef(onClose);
|
|
22
|
+
onCloseRef.current = onClose;
|
|
23
|
+
const { syncing, syncResult, syncNow } = useSyncAction(onSyncStatusRefresh);
|
|
24
|
+
|
|
25
|
+
// Close on ESC
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!open) return;
|
|
28
|
+
const handler = (e: KeyboardEvent) => {
|
|
29
|
+
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); onCloseRef.current(); }
|
|
30
|
+
};
|
|
31
|
+
window.addEventListener('keydown', handler);
|
|
32
|
+
return () => window.removeEventListener('keydown', handler);
|
|
33
|
+
}, [open]);
|
|
34
|
+
|
|
35
|
+
// Close on click outside
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!open) return;
|
|
38
|
+
const handler = (e: MouseEvent) => {
|
|
39
|
+
if (ref.current && !ref.current.contains(e.target as Node)) onCloseRef.current();
|
|
40
|
+
};
|
|
41
|
+
const id = setTimeout(() => window.addEventListener('mousedown', handler), 0);
|
|
42
|
+
return () => { clearTimeout(id); window.removeEventListener('mousedown', handler); };
|
|
43
|
+
}, [open]);
|
|
44
|
+
|
|
45
|
+
if (!open || !anchorRect) return null;
|
|
46
|
+
|
|
47
|
+
const level = getStatusLevel(syncStatus, syncing);
|
|
48
|
+
const { label: statusText } = getSyncLabel(level, syncStatus);
|
|
49
|
+
|
|
50
|
+
// Position: anchor near the button, avoid going off-screen top
|
|
51
|
+
const popoverTop = Math.max(8, anchorRect.bottom - 180);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
ref={ref}
|
|
56
|
+
className="fixed z-40 w-[240px] border rounded-lg bg-card shadow-lg border-border animate-in fade-in slide-in-from-left-2 duration-150"
|
|
57
|
+
style={{
|
|
58
|
+
top: popoverTop,
|
|
59
|
+
left: railWidth,
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-border">
|
|
63
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Sync</span>
|
|
64
|
+
<button
|
|
65
|
+
onClick={onClose}
|
|
66
|
+
className="p-0.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
|
67
|
+
aria-label="Close"
|
|
68
|
+
>
|
|
69
|
+
<X size={14} />
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="p-3 space-y-3">
|
|
73
|
+
{/* Status */}
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${DOT_COLORS[level]} ${
|
|
76
|
+
level === 'syncing' || level === 'conflicts' || level === 'error' ? 'animate-pulse' : ''
|
|
77
|
+
}`} />
|
|
78
|
+
<span className="text-sm text-foreground">{statusText}</span>
|
|
79
|
+
{syncResult === 'success' && <CheckCircle2 size={14} className="text-success shrink-0" />}
|
|
80
|
+
{syncResult === 'error' && <XCircle size={14} className="text-error shrink-0" />}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Actions */}
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
{level !== 'off' && (
|
|
86
|
+
<PrimaryButton
|
|
87
|
+
onClick={syncNow}
|
|
88
|
+
disabled={syncing}
|
|
89
|
+
className="text-xs px-3 py-1.5 flex items-center gap-1.5"
|
|
90
|
+
>
|
|
91
|
+
<RefreshCw size={12} className={syncing ? 'animate-spin' : ''} />
|
|
92
|
+
Sync Now
|
|
93
|
+
</PrimaryButton>
|
|
94
|
+
)}
|
|
95
|
+
<button
|
|
96
|
+
onClick={() => { onOpenSyncSettings(); onClose(); }}
|
|
97
|
+
className="text-xs text-muted-foreground hover:text-foreground transition-colors rounded px-1 py-0.5 focus-visible:ring-2 focus-visible:ring-ring"
|
|
98
|
+
>
|
|
99
|
+
Settings →
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useMemo, useEffect } from 'react';
|
|
3
|
+
import React, { useState, useMemo, useEffect } from 'react';
|
|
4
4
|
import { ChevronUp, ChevronDown, Plus, Trash2 } from 'lucide-react';
|
|
5
5
|
import type { TableConfig } from './types';
|
|
6
6
|
import { serializeCSV } from './types';
|
|
@@ -102,8 +102,8 @@ export function TableView({ headers, rows, cfg, saveAction }: {
|
|
|
102
102
|
</tr>
|
|
103
103
|
</thead>
|
|
104
104
|
<tbody>
|
|
105
|
-
{sections.map(section => (
|
|
106
|
-
|
|
105
|
+
{sections.map((section, si) => (
|
|
106
|
+
<React.Fragment key={section.key ?? `section-${si}`}>
|
|
107
107
|
{section.key !== null && (
|
|
108
108
|
<tr key={`grp-${section.key}`}>
|
|
109
109
|
<td colSpan={visibleIndices.length + 1} className="px-4 py-1.5"
|
|
@@ -137,7 +137,7 @@ export function TableView({ headers, rows, cfg, saveAction }: {
|
|
|
137
137
|
</tr>
|
|
138
138
|
);
|
|
139
139
|
})}
|
|
140
|
-
|
|
140
|
+
</React.Fragment>
|
|
141
141
|
))}
|
|
142
142
|
{showAdd && (
|
|
143
143
|
<AddRowTr headers={headers} visibleIndices={visibleIndices} onAdd={addRow} onCancel={() => setShowAdd(false)} />
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
4
|
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
5
5
|
import type { AiSettings, AgentSettings, ProviderConfig, SettingsData, AiTabProps } from './types';
|
|
6
|
-
import { Field, Select, Input, EnvBadge, ApiKeyInput, Toggle } from './Primitives';
|
|
6
|
+
import { Field, Select, Input, EnvBadge, ApiKeyInput, Toggle, SectionLabel } from './Primitives';
|
|
7
7
|
|
|
8
8
|
type TestState = 'idle' | 'testing' | 'ok' | 'error';
|
|
9
9
|
type ErrorCode = 'auth_error' | 'model_not_found' | 'rate_limited' | 'network_error' | 'unknown';
|
|
@@ -277,6 +277,44 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
277
277
|
)}
|
|
278
278
|
</div>
|
|
279
279
|
</div>
|
|
280
|
+
|
|
281
|
+
{/* Ask AI Display Mode */}
|
|
282
|
+
<AskDisplayMode />
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ── Ask AI Display Mode (localStorage-based, no server roundtrip) ── */
|
|
288
|
+
|
|
289
|
+
function AskDisplayMode() {
|
|
290
|
+
const [mode, setMode] = useState<'panel' | 'popup'>('panel');
|
|
291
|
+
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
try {
|
|
294
|
+
const stored = localStorage.getItem('ask-mode');
|
|
295
|
+
if (stored === 'popup') setMode('popup');
|
|
296
|
+
} catch {}
|
|
297
|
+
}, []);
|
|
298
|
+
|
|
299
|
+
const handleChange = (value: string) => {
|
|
300
|
+
const next = value as 'panel' | 'popup';
|
|
301
|
+
setMode(next);
|
|
302
|
+
try { localStorage.setItem('ask-mode', next); } catch {}
|
|
303
|
+
// Notify SidebarLayout to pick up the change
|
|
304
|
+
window.dispatchEvent(new StorageEvent('storage', { key: 'ask-mode', newValue: next }));
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div className="pt-3 border-t border-border">
|
|
309
|
+
<SectionLabel>MindOS Agent</SectionLabel>
|
|
310
|
+
<div className="space-y-4">
|
|
311
|
+
<Field label="Display Mode" hint="Side panel stays docked on the right. Popup opens a floating dialog.">
|
|
312
|
+
<Select value={mode} onChange={e => handleChange(e.target.value)}>
|
|
313
|
+
<option value="panel">Side Panel</option>
|
|
314
|
+
<option value="popup">Popup</option>
|
|
315
|
+
</Select>
|
|
316
|
+
</Field>
|
|
317
|
+
</div>
|
|
280
318
|
</div>
|
|
281
319
|
);
|
|
282
320
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback, useSyncExternalStore } from 'react';
|
|
4
|
-
import { Copy, Check, RefreshCw, Trash2, Sparkles } from 'lucide-react';
|
|
3
|
+
import { useState, useEffect, useCallback, useSyncExternalStore, useRef } from 'react';
|
|
4
|
+
import { Copy, Check, RefreshCw, Trash2, Sparkles, ChevronDown, ChevronRight, Loader2, Cpu, Zap, Database as DatabaseIcon, HardDrive } from 'lucide-react';
|
|
5
5
|
import type { KnowledgeTabProps } from './types';
|
|
6
6
|
import { Field, Input, EnvBadge, SectionLabel, Toggle } from './Primitives';
|
|
7
7
|
import { apiFetch } from '@/lib/api';
|
|
8
|
+
import { formatBytes, formatUptime } from '@/lib/format';
|
|
8
9
|
|
|
9
10
|
export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
10
11
|
const env = data.envOverrides ?? {};
|
|
@@ -205,6 +206,119 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
205
206
|
</div>
|
|
206
207
|
</div>
|
|
207
208
|
)}
|
|
209
|
+
|
|
210
|
+
{/* System Monitoring — collapsible */}
|
|
211
|
+
<MonitoringSection />
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ── Inline Monitoring Section ── */
|
|
217
|
+
|
|
218
|
+
interface MonitoringData {
|
|
219
|
+
system: {
|
|
220
|
+
uptimeMs: number;
|
|
221
|
+
memory: { heapUsed: number; heapTotal: number; rss: number };
|
|
222
|
+
nodeVersion: string;
|
|
223
|
+
};
|
|
224
|
+
application: {
|
|
225
|
+
agentRequests: number;
|
|
226
|
+
toolExecutions: number;
|
|
227
|
+
totalTokens: { input: number; output: number };
|
|
228
|
+
avgResponseTimeMs: number;
|
|
229
|
+
errors: number;
|
|
230
|
+
};
|
|
231
|
+
knowledgeBase: {
|
|
232
|
+
root: string;
|
|
233
|
+
fileCount: number;
|
|
234
|
+
totalSizeBytes: number;
|
|
235
|
+
};
|
|
236
|
+
mcp: { running: boolean; port: number };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function MonitoringSection() {
|
|
240
|
+
const [expanded, setExpanded] = useState(false);
|
|
241
|
+
const [data, setData] = useState<MonitoringData | null>(null);
|
|
242
|
+
const [loading, setLoading] = useState(false);
|
|
243
|
+
|
|
244
|
+
const fetchData = useCallback(async () => {
|
|
245
|
+
setLoading(true);
|
|
246
|
+
try {
|
|
247
|
+
const d = await apiFetch<MonitoringData>('/api/monitoring', { timeout: 5000 });
|
|
248
|
+
setData(d);
|
|
249
|
+
} catch { /* ignore */ }
|
|
250
|
+
setLoading(false);
|
|
251
|
+
}, []);
|
|
252
|
+
|
|
253
|
+
// Fetch on first expand, then refresh every 10s while expanded
|
|
254
|
+
const hasFetched = useRef(false);
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (!expanded) { hasFetched.current = false; return; }
|
|
257
|
+
if (!hasFetched.current) { fetchData(); hasFetched.current = true; }
|
|
258
|
+
const id = setInterval(fetchData, 10_000);
|
|
259
|
+
return () => clearInterval(id);
|
|
260
|
+
}, [expanded, fetchData]);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div className="border-t border-border pt-5">
|
|
264
|
+
<button
|
|
265
|
+
onClick={() => setExpanded(v => !v)}
|
|
266
|
+
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors w-full"
|
|
267
|
+
>
|
|
268
|
+
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
269
|
+
<Cpu size={12} />
|
|
270
|
+
System Monitoring
|
|
271
|
+
{loading && <Loader2 size={10} className="animate-spin ml-1" />}
|
|
272
|
+
</button>
|
|
273
|
+
|
|
274
|
+
{expanded && data && (
|
|
275
|
+
<div className="mt-3 grid grid-cols-2 gap-x-6 gap-y-2 text-xs">
|
|
276
|
+
<div>
|
|
277
|
+
<span className="text-muted-foreground">Heap</span>
|
|
278
|
+
<span className="ml-2 tabular-nums">{formatBytes(data.system.memory.heapUsed)} / {formatBytes(data.system.memory.heapTotal)}</span>
|
|
279
|
+
</div>
|
|
280
|
+
<div>
|
|
281
|
+
<span className="text-muted-foreground">RSS</span>
|
|
282
|
+
<span className="ml-2 tabular-nums">{formatBytes(data.system.memory.rss)}</span>
|
|
283
|
+
</div>
|
|
284
|
+
<div>
|
|
285
|
+
<span className="text-muted-foreground">Uptime</span>
|
|
286
|
+
<span className="ml-2 tabular-nums">{formatUptime(data.system.uptimeMs)}</span>
|
|
287
|
+
</div>
|
|
288
|
+
<div>
|
|
289
|
+
<span className="text-muted-foreground">Node</span>
|
|
290
|
+
<span className="ml-2">{data.system.nodeVersion}</span>
|
|
291
|
+
</div>
|
|
292
|
+
<div>
|
|
293
|
+
<span className="text-muted-foreground">Requests</span>
|
|
294
|
+
<span className="ml-2 tabular-nums">{data.application.agentRequests}</span>
|
|
295
|
+
</div>
|
|
296
|
+
<div>
|
|
297
|
+
<span className="text-muted-foreground">Tool Calls</span>
|
|
298
|
+
<span className="ml-2 tabular-nums">{data.application.toolExecutions}</span>
|
|
299
|
+
</div>
|
|
300
|
+
<div>
|
|
301
|
+
<span className="text-muted-foreground">Tokens</span>
|
|
302
|
+
<span className="ml-2 tabular-nums">{(data.application.totalTokens.input + data.application.totalTokens.output).toLocaleString()}</span>
|
|
303
|
+
</div>
|
|
304
|
+
<div>
|
|
305
|
+
<span className="text-muted-foreground">Files</span>
|
|
306
|
+
<span className="ml-2 tabular-nums">{data.knowledgeBase.fileCount} ({formatBytes(data.knowledgeBase.totalSizeBytes)})</span>
|
|
307
|
+
</div>
|
|
308
|
+
<div>
|
|
309
|
+
<span className="text-muted-foreground">MCP</span>
|
|
310
|
+
<span className="ml-2">{data.mcp.running ? `Running :${data.mcp.port}` : 'Stopped'}</span>
|
|
311
|
+
</div>
|
|
312
|
+
<div>
|
|
313
|
+
<span className="text-muted-foreground">Errors</span>
|
|
314
|
+
<span className="ml-2 tabular-nums">{data.application.errors}</span>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{expanded && !data && !loading && (
|
|
320
|
+
<p className="mt-2 text-xs text-muted-foreground">Failed to load monitoring data</p>
|
|
321
|
+
)}
|
|
208
322
|
</div>
|
|
209
323
|
);
|
|
210
324
|
}
|
|
@@ -47,17 +47,17 @@ export function McpTab({ t }: McpTabProps) {
|
|
|
47
47
|
<ServerStatus status={mcpStatus} agents={agents} t={t} />
|
|
48
48
|
</div>
|
|
49
49
|
|
|
50
|
-
{/* Agent Configuration */}
|
|
51
|
-
<div>
|
|
52
|
-
<h3 className="text-sm font-medium text-foreground mb-3">{m?.agentsTitle ?? 'Agent Configuration'}</h3>
|
|
53
|
-
<AgentInstall agents={agents} t={t} onRefresh={fetchAll} />
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
50
|
{/* Skills */}
|
|
57
51
|
<div>
|
|
58
52
|
<h3 className="text-sm font-medium text-foreground mb-3">{m?.skillsTitle ?? 'Skills'}</h3>
|
|
59
53
|
<SkillsSection t={t} />
|
|
60
54
|
</div>
|
|
55
|
+
|
|
56
|
+
{/* Agent Configuration */}
|
|
57
|
+
<div>
|
|
58
|
+
<h3 className="text-sm font-medium text-foreground mb-3">{m?.agentsTitle ?? 'Agent Configuration'}</h3>
|
|
59
|
+
<AgentInstall agents={agents} t={t} onRefresh={fetchAll} />
|
|
60
|
+
</div>
|
|
61
61
|
</div>
|
|
62
62
|
);
|
|
63
63
|
}
|