@schlessera/brain-ui-react 0.5.0 → 0.6.0
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/dist/components/chat/chat-page.d.ts.map +1 -1
- package/dist/components/chat/chat-page.js +23 -11
- package/dist/components/chat/chat-page.js.map +1 -1
- package/dist/components/layout/kbd.d.ts +6 -0
- package/dist/components/layout/kbd.d.ts.map +1 -0
- package/dist/components/layout/kbd.js +6 -0
- package/dist/components/layout/kbd.js.map +1 -0
- package/dist/components/quick-actions/add-modal.d.ts +5 -0
- package/dist/components/quick-actions/add-modal.d.ts.map +1 -0
- package/dist/components/quick-actions/add-modal.js +91 -0
- package/dist/components/quick-actions/add-modal.js.map +1 -0
- package/dist/components/quick-actions/search-modal.d.ts +5 -0
- package/dist/components/quick-actions/search-modal.d.ts.map +1 -0
- package/dist/components/quick-actions/search-modal.js +134 -0
- package/dist/components/quick-actions/search-modal.js.map +1 -0
- package/dist/components/settings/models-tab.d.ts.map +1 -1
- package/dist/components/settings/models-tab.js +9 -0
- package/dist/components/settings/models-tab.js.map +1 -1
- package/dist/lib/api-client.d.ts +22 -9
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +9 -1
- package/dist/lib/api-client.js.map +1 -1
- package/dist/lib/search-snippet.d.ts +15 -0
- package/dist/lib/search-snippet.d.ts.map +1 -0
- package/dist/lib/search-snippet.js +45 -0
- package/dist/lib/search-snippet.js.map +1 -0
- package/dist/stores/provider-store.d.ts.map +1 -1
- package/dist/stores/provider-store.js +10 -1
- package/dist/stores/provider-store.js.map +1 -1
- package/dist/stores/ui-store.d.ts +6 -0
- package/dist/stores/ui-store.d.ts.map +1 -1
- package/dist/stores/ui-store.js +6 -0
- package/dist/stores/ui-store.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/src/components/chat/chat-page.tsx +32 -10
- package/src/components/layout/kbd.tsx +10 -0
- package/src/components/quick-actions/add-modal.tsx +231 -0
- package/src/components/quick-actions/search-modal.tsx +285 -0
- package/src/components/settings/models-tab.tsx +9 -0
- package/src/lib/api-client.ts +40 -6
- package/src/lib/search-snippet.ts +64 -0
- package/src/stores/provider-store.ts +12 -1
- package/src/stores/ui-store.ts +14 -0
package/src/lib/api-client.ts
CHANGED
|
@@ -15,6 +15,26 @@ import type {
|
|
|
15
15
|
PublicKeyCredentialRequestOptionsJSON,
|
|
16
16
|
} from "@simplewebauthn/browser";
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* One hit from `brain search`. `snippet` carries the CLI's FTS highlight
|
|
20
|
+
* markers (`>>>term<<<`) — render it through `renderSnippet()` in the search
|
|
21
|
+
* panel rather than printing it raw.
|
|
22
|
+
*/
|
|
23
|
+
export interface BrainSearchHit {
|
|
24
|
+
path: string;
|
|
25
|
+
title: string;
|
|
26
|
+
type: string;
|
|
27
|
+
relevance: string;
|
|
28
|
+
score: number;
|
|
29
|
+
snippet: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BrainSearchResponse {
|
|
33
|
+
results: BrainSearchHit[];
|
|
34
|
+
/** Degraded-mode notices, e.g. vector search unavailable so results are FTS-only. */
|
|
35
|
+
warnings: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
/** Backend id + capability flags the client renders behavior from. */
|
|
19
39
|
export interface BackendInfo {
|
|
20
40
|
id: string;
|
|
@@ -58,12 +78,26 @@ export const api = {
|
|
|
58
78
|
activeSession: boolean;
|
|
59
79
|
}>("/status"),
|
|
60
80
|
|
|
61
|
-
brainSearch: (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
brainSearch: (
|
|
82
|
+
q: string,
|
|
83
|
+
opts?: {
|
|
84
|
+
type?: string;
|
|
85
|
+
tag?: string;
|
|
86
|
+
limit?: number;
|
|
87
|
+
mode?: "fts" | "vector" | "hybrid";
|
|
88
|
+
signal?: AbortSignal;
|
|
89
|
+
}
|
|
90
|
+
) =>
|
|
91
|
+
// Built with encodeURIComponent rather than URLSearchParams: the latter
|
|
92
|
+
// form-encodes spaces as "+", which not every query parser turns back into
|
|
93
|
+
// a space. %20 is unambiguous.
|
|
94
|
+
fetchJson<BrainSearchResponse>(
|
|
95
|
+
`/brain/search?q=${encodeURIComponent(q)}` +
|
|
96
|
+
(opts?.type ? `&type=${encodeURIComponent(opts.type)}` : "") +
|
|
97
|
+
(opts?.tag ? `&tag=${encodeURIComponent(opts.tag)}` : "") +
|
|
98
|
+
(opts?.limit ? `&limit=${opts.limit}` : "") +
|
|
99
|
+
(opts?.mode ? `&mode=${opts.mode}` : ""),
|
|
100
|
+
{ signal: opts?.signal }
|
|
67
101
|
),
|
|
68
102
|
|
|
69
103
|
brainBriefing: () =>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { stripMarkdown } from "./strip-markdown.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `brain search` returns FTS snippets with the matched terms wrapped in
|
|
5
|
+
* `>>>` / `<<<` markers (sqlite's snippet() delimiters, chosen by the CLI so
|
|
6
|
+
* they survive JSON without colliding with markdown). Vector hits come back
|
|
7
|
+
* unmarked, as a raw slice of chunk text — headings, emphasis, blank lines and
|
|
8
|
+
* all. Split a snippet into plain and matched segments, with the markdown
|
|
9
|
+
* syntax stripped and the whitespace collapsed, so a result row is one legible
|
|
10
|
+
* line with its hits highlighted.
|
|
11
|
+
*/
|
|
12
|
+
export interface SnippetSegment {
|
|
13
|
+
text: string;
|
|
14
|
+
hit: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const OPEN = ">>>";
|
|
18
|
+
const CLOSE = "<<<";
|
|
19
|
+
// Control characters stand in for the markers while stripMarkdown runs: a
|
|
20
|
+
// snippet starting with ">>>" would otherwise lose a ">" to the blockquote
|
|
21
|
+
// rule, and emphasis spanning a highlight (`**a >>>b<<<**`) has to be
|
|
22
|
+
// unwrapped as one string, not per segment.
|
|
23
|
+
const OPEN_MARK = "\u0001";
|
|
24
|
+
const CLOSE_MARK = "\u0002";
|
|
25
|
+
|
|
26
|
+
export function parseSnippet(snippet: string): SnippetSegment[] {
|
|
27
|
+
const cleaned = stripMarkdown(
|
|
28
|
+
snippet.split(OPEN).join(OPEN_MARK).split(CLOSE).join(CLOSE_MARK)
|
|
29
|
+
)
|
|
30
|
+
.replace(/\s+/g, " ")
|
|
31
|
+
.trim();
|
|
32
|
+
|
|
33
|
+
const segments: SnippetSegment[] = [];
|
|
34
|
+
let rest = cleaned;
|
|
35
|
+
|
|
36
|
+
while (rest.length > 0) {
|
|
37
|
+
const start = rest.indexOf(OPEN_MARK);
|
|
38
|
+
if (start === -1) break;
|
|
39
|
+
|
|
40
|
+
const end = rest.indexOf(CLOSE_MARK, start + 1);
|
|
41
|
+
// An unclosed marker means the snippet was clipped mid-highlight; keep the
|
|
42
|
+
// remainder as plain text rather than dropping it.
|
|
43
|
+
if (end === -1) break;
|
|
44
|
+
|
|
45
|
+
if (start > 0) push(segments, rest.slice(0, start), false);
|
|
46
|
+
push(segments, rest.slice(start + 1, end), true);
|
|
47
|
+
rest = rest.slice(end + 1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
push(segments, rest, false);
|
|
51
|
+
return segments;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function push(segments: SnippetSegment[], text: string, hit: boolean): void {
|
|
55
|
+
// Any stray marker left by a clipped snippet is noise, not content —
|
|
56
|
+
// squeeze the gap it leaves behind so the row reads as one sentence.
|
|
57
|
+
const clean = text
|
|
58
|
+
.split(OPEN_MARK)
|
|
59
|
+
.join("")
|
|
60
|
+
.split(CLOSE_MARK)
|
|
61
|
+
.join("")
|
|
62
|
+
.replace(/ {2,}/g, " ");
|
|
63
|
+
if (clean) segments.push({ text: clean, hit });
|
|
64
|
+
}
|
|
@@ -51,12 +51,23 @@ export const useProviderStore = create<ProviderState>((set, get) => ({
|
|
|
51
51
|
const { providers, backends } = await api.providers();
|
|
52
52
|
set((s) => {
|
|
53
53
|
// Keep the persisted choice if it's still on offer; otherwise fall
|
|
54
|
-
// back to the first available combo.
|
|
54
|
+
// back to the first available combo. This runs on every reload of the
|
|
55
|
+
// roster, so hiding the selected model in settings moves the selection
|
|
56
|
+
// instead of leaving the composer pointing at a profile the picker no
|
|
57
|
+
// longer lists.
|
|
55
58
|
const stillValid = providers.some((p) => p.id === s.selectedId);
|
|
56
59
|
const selectedId =
|
|
57
60
|
stillValid && s.selectedId
|
|
58
61
|
? s.selectedId
|
|
59
62
|
: providers[0]?.id ?? "";
|
|
63
|
+
// Persist the fallback too — otherwise the stale id sits in
|
|
64
|
+
// localStorage and has to be re-resolved on every boot.
|
|
65
|
+
if (
|
|
66
|
+
selectedId !== s.selectedId &&
|
|
67
|
+
typeof localStorage !== "undefined"
|
|
68
|
+
) {
|
|
69
|
+
localStorage.setItem(PROVIDER_ID_KEY, selectedId);
|
|
70
|
+
}
|
|
60
71
|
return {
|
|
61
72
|
available: providers,
|
|
62
73
|
selectedId,
|
package/src/stores/ui-store.ts
CHANGED
|
@@ -7,17 +7,23 @@ interface UIState {
|
|
|
7
7
|
sessionPanelOpen: boolean;
|
|
8
8
|
syncPanelOpen: boolean;
|
|
9
9
|
whatsupPanelOpen: boolean;
|
|
10
|
+
searchPanelOpen: boolean;
|
|
11
|
+
addPanelOpen: boolean;
|
|
10
12
|
filePanelOpen: boolean;
|
|
11
13
|
settingsPanelOpen: boolean;
|
|
12
14
|
settingsTab: SettingsTab;
|
|
13
15
|
toggleSessionPanel: () => void;
|
|
14
16
|
toggleSyncPanel: () => void;
|
|
15
17
|
toggleWhatsupPanel: () => void;
|
|
18
|
+
toggleSearchPanel: () => void;
|
|
19
|
+
toggleAddPanel: () => void;
|
|
16
20
|
toggleFilePanel: () => void;
|
|
17
21
|
toggleSettingsPanel: () => void;
|
|
18
22
|
closeAllPanels: () => void;
|
|
19
23
|
setSyncPanelOpen: (open: boolean) => void;
|
|
20
24
|
setWhatsupPanelOpen: (open: boolean) => void;
|
|
25
|
+
setSearchPanelOpen: (open: boolean) => void;
|
|
26
|
+
setAddPanelOpen: (open: boolean) => void;
|
|
21
27
|
setSessionPanelOpen: (open: boolean) => void;
|
|
22
28
|
setFilePanelOpen: (open: boolean) => void;
|
|
23
29
|
setSettingsPanelOpen: (open: boolean) => void;
|
|
@@ -30,6 +36,8 @@ const CLOSED = {
|
|
|
30
36
|
sessionPanelOpen: false,
|
|
31
37
|
syncPanelOpen: false,
|
|
32
38
|
whatsupPanelOpen: false,
|
|
39
|
+
searchPanelOpen: false,
|
|
40
|
+
addPanelOpen: false,
|
|
33
41
|
filePanelOpen: false,
|
|
34
42
|
settingsPanelOpen: false,
|
|
35
43
|
};
|
|
@@ -43,6 +51,10 @@ export const useUIStore = create<UIState>((set) => ({
|
|
|
43
51
|
set((s) => ({ ...CLOSED, syncPanelOpen: !s.syncPanelOpen })),
|
|
44
52
|
toggleWhatsupPanel: () =>
|
|
45
53
|
set((s) => ({ ...CLOSED, whatsupPanelOpen: !s.whatsupPanelOpen })),
|
|
54
|
+
toggleSearchPanel: () =>
|
|
55
|
+
set((s) => ({ ...CLOSED, searchPanelOpen: !s.searchPanelOpen })),
|
|
56
|
+
toggleAddPanel: () =>
|
|
57
|
+
set((s) => ({ ...CLOSED, addPanelOpen: !s.addPanelOpen })),
|
|
46
58
|
toggleFilePanel: () =>
|
|
47
59
|
set((s) => ({ ...CLOSED, filePanelOpen: !s.filePanelOpen })),
|
|
48
60
|
toggleSettingsPanel: () =>
|
|
@@ -50,6 +62,8 @@ export const useUIStore = create<UIState>((set) => ({
|
|
|
50
62
|
closeAllPanels: () => set({ ...CLOSED }),
|
|
51
63
|
setSyncPanelOpen: (open) => set({ syncPanelOpen: open }),
|
|
52
64
|
setWhatsupPanelOpen: (open) => set({ whatsupPanelOpen: open }),
|
|
65
|
+
setSearchPanelOpen: (open) => set({ searchPanelOpen: open }),
|
|
66
|
+
setAddPanelOpen: (open) => set({ addPanelOpen: open }),
|
|
53
67
|
setSessionPanelOpen: (open) => set({ sessionPanelOpen: open }),
|
|
54
68
|
setFilePanelOpen: (open) => set({ filePanelOpen: open }),
|
|
55
69
|
setSettingsPanelOpen: (open) => set({ settingsPanelOpen: open }),
|