mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-24 20:28:16 +02:00
knowledge to brain
This commit is contained in:
parent
1f8ac2cf34
commit
811ae22bb1
4 changed files with 117 additions and 43 deletions
|
|
@ -30,7 +30,7 @@ import { BgTasksView } from '@/components/bg-tasks-view';
|
|||
import { EmailView } from '@/components/email-view';
|
||||
import { WorkspaceView } from '@/components/workspace-view';
|
||||
import { CodingRunBlock } from '@/components/coding-run';
|
||||
import { KnowledgeView } from '@/components/knowledge-view';
|
||||
import { KnowledgeView, type KnowledgeViewMode } from '@/components/knowledge-view';
|
||||
import { ChatHistoryView } from '@/components/chat-history-view';
|
||||
import { HomeView } from '@/components/home-view';
|
||||
import { MeetingsView } from '@/components/meetings-view';
|
||||
|
|
@ -591,7 +591,7 @@ type ViewState =
|
|||
| { type: 'live-notes' }
|
||||
| { type: 'email' }
|
||||
| { type: 'workspace'; path?: string }
|
||||
| { type: 'knowledge-view'; folderPath?: string }
|
||||
| { type: 'knowledge-view'; folderPath?: string; mode?: KnowledgeViewMode }
|
||||
| { type: 'chat-history' }
|
||||
| { type: 'home' }
|
||||
| { type: 'code' }
|
||||
|
|
@ -602,7 +602,7 @@ function viewStatesEqual(a: ViewState, b: ViewState): boolean {
|
|||
if (a.type === 'file' && b.type === 'file') return a.path === b.path
|
||||
if (a.type === 'task' && b.type === 'task') return a.name === b.name
|
||||
if (a.type === 'workspace' && b.type === 'workspace') return (a.path ?? '') === (b.path ?? '')
|
||||
if (a.type === 'knowledge-view' && b.type === 'knowledge-view') return (a.folderPath ?? '') === (b.folderPath ?? '')
|
||||
if (a.type === 'knowledge-view' && b.type === 'knowledge-view') return (a.folderPath ?? '') === (b.folderPath ?? '') && (a.mode ?? '') === (b.mode ?? '')
|
||||
return true // both graph
|
||||
}
|
||||
|
||||
|
|
@ -652,7 +652,12 @@ function parseDeepLink(input: string): ViewState | null {
|
|||
}
|
||||
case 'knowledge-view': {
|
||||
const folderPath = params.get('folderPath')
|
||||
return { type: 'knowledge-view', folderPath: folderPath ?? undefined }
|
||||
const mode = params.get('mode')
|
||||
return {
|
||||
type: 'knowledge-view',
|
||||
folderPath: folderPath ?? undefined,
|
||||
mode: mode === 'graph' || mode === 'basis' || mode === 'files' ? mode : undefined,
|
||||
}
|
||||
}
|
||||
case 'chat-history':
|
||||
return { type: 'chat-history' }
|
||||
|
|
@ -775,6 +780,7 @@ function App() {
|
|||
const [isWorkspaceOpen, setIsWorkspaceOpen] = useState(false)
|
||||
const [workspaceInitialPath, setWorkspaceInitialPath] = useState<string | null>(null)
|
||||
const [isKnowledgeViewOpen, setIsKnowledgeViewOpen] = useState(false)
|
||||
const [knowledgeViewMode, setKnowledgeViewMode] = useState<KnowledgeViewMode>('graph')
|
||||
// Folder being browsed inside the knowledge view (null = root overview).
|
||||
// Lives in ViewState so folder drill-down participates in back/forward history.
|
||||
const [knowledgeViewFolderPath, setKnowledgeViewFolderPath] = useState<string | null>(null)
|
||||
|
|
@ -1197,7 +1203,7 @@ function App() {
|
|||
if (isBgTasksTabPath(tab.path)) return 'Background tasks'
|
||||
if (isEmailTabPath(tab.path)) return 'Email'
|
||||
if (isWorkspaceTabPath(tab.path)) return 'Workspace'
|
||||
if (isKnowledgeViewTabPath(tab.path)) return 'Notes'
|
||||
if (isKnowledgeViewTabPath(tab.path)) return 'Brain'
|
||||
if (isChatHistoryTabPath(tab.path)) return 'Chat history'
|
||||
if (isHomeTabPath(tab.path)) return 'Home'
|
||||
if (isCodeTabPath(tab.path)) return 'Code'
|
||||
|
|
@ -3627,14 +3633,14 @@ function App() {
|
|||
if (isLiveNotesOpen) return { type: 'live-notes' }
|
||||
if (isSuggestedTopicsOpen) return { type: 'suggested-topics' }
|
||||
if (isWorkspaceOpen) return { type: 'workspace', path: workspaceInitialPath ?? undefined }
|
||||
if (isKnowledgeViewOpen) return { type: 'knowledge-view', folderPath: knowledgeViewFolderPath ?? undefined }
|
||||
if (isKnowledgeViewOpen) return { type: 'knowledge-view', folderPath: knowledgeViewFolderPath ?? undefined, mode: knowledgeViewMode }
|
||||
if (isChatHistoryOpen) return { type: 'chat-history' }
|
||||
if (isHomeOpen) return { type: 'home' }
|
||||
if (isCodeOpen) return { type: 'code' }
|
||||
if (selectedPath) return { type: 'file', path: selectedPath }
|
||||
if (isGraphOpen) return { type: 'graph' }
|
||||
return { type: 'chat', runId }
|
||||
}, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, isWorkspaceOpen, isKnowledgeViewOpen, knowledgeViewFolderPath, isChatHistoryOpen, isHomeOpen, isCodeOpen, workspaceInitialPath, runId])
|
||||
}, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, isWorkspaceOpen, isKnowledgeViewOpen, knowledgeViewFolderPath, knowledgeViewMode, isChatHistoryOpen, isHomeOpen, isCodeOpen, workspaceInitialPath, runId])
|
||||
|
||||
const appendUnique = useCallback((stack: ViewState[], entry: ViewState) => {
|
||||
const last = stack[stack.length - 1]
|
||||
|
|
@ -3997,6 +4003,7 @@ function App() {
|
|||
setIsEmailOpen(false)
|
||||
setIsWorkspaceOpen(false)
|
||||
setIsKnowledgeViewOpen(true)
|
||||
setKnowledgeViewMode(view.mode ?? (view.folderPath ? 'files' : 'graph'))
|
||||
setKnowledgeViewFolderPath(view.folderPath ?? null)
|
||||
setIsChatHistoryOpen(false)
|
||||
setIsHomeOpen(false)
|
||||
|
|
@ -4223,10 +4230,9 @@ function App() {
|
|||
setBaseConfigByPath((prev) => ({ ...prev, [path]: config }))
|
||||
}, [])
|
||||
|
||||
const handleBaseSave = useCallback(async (name: string | null) => {
|
||||
if (!selectedPath) return
|
||||
const isDefault = selectedPath === BASES_DEFAULT_TAB_PATH
|
||||
const config = baseConfigByPath[selectedPath] ?? DEFAULT_BASE_CONFIG
|
||||
const handleBaseSave = useCallback(async (path: string, name: string | null) => {
|
||||
const isDefault = path === BASES_DEFAULT_TAB_PATH
|
||||
const config = baseConfigByPath[path] ?? DEFAULT_BASE_CONFIG
|
||||
|
||||
if (isDefault && name) {
|
||||
// Save as new base file
|
||||
|
|
@ -4250,14 +4256,14 @@ function App() {
|
|||
// Save in place
|
||||
try {
|
||||
await window.ipc.invoke('workspace:writeFile', {
|
||||
path: selectedPath,
|
||||
path,
|
||||
data: JSON.stringify(config, null, 2),
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to save base:', err)
|
||||
}
|
||||
}
|
||||
}, [selectedPath, baseConfigByPath, loadDirectory, navigateToView])
|
||||
}, [baseConfigByPath, loadDirectory, navigateToView])
|
||||
|
||||
// External search set by app-navigation tool (passed to BasesView)
|
||||
const [externalBaseSearch, setExternalBaseSearch] = useState<string | undefined>(undefined)
|
||||
|
|
@ -5121,8 +5127,10 @@ function App() {
|
|||
},
|
||||
}), [knowledgeFiles, recentWikiFiles, openWikiLink, ensureWikiFile])
|
||||
|
||||
const isBrainGraphOpen = isKnowledgeViewOpen && knowledgeViewMode === 'graph'
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGraphOpen) return
|
||||
if (!isGraphOpen && !isBrainGraphOpen) return
|
||||
let cancelled = false
|
||||
|
||||
const buildGraph = async () => {
|
||||
|
|
@ -5237,7 +5245,7 @@ function App() {
|
|||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [isGraphOpen, knowledgeFilePaths])
|
||||
}, [isGraphOpen, isBrainGraphOpen, knowledgeFilePaths])
|
||||
|
||||
const renderConversationItem = (
|
||||
item: ConversationItem,
|
||||
|
|
@ -5760,12 +5768,44 @@ function App() {
|
|||
revealInFileManager: knowledgeActions.revealInFileManager,
|
||||
onOpenInNewTab: knowledgeActions.onOpenInNewTab,
|
||||
}}
|
||||
mode={knowledgeViewMode}
|
||||
onModeChange={setKnowledgeViewMode}
|
||||
graphContent={(
|
||||
<GraphView
|
||||
nodes={graphData.nodes}
|
||||
edges={graphData.edges}
|
||||
isLoading={false}
|
||||
error={graphStatus === 'error' ? (graphError ?? 'Failed to build graph') : null}
|
||||
onSelectNode={(path) => {
|
||||
navigateToFile(path)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
basisContent={(
|
||||
<BasesView
|
||||
tree={tree}
|
||||
onSelectNote={(path) => navigateToFile(path)}
|
||||
config={baseConfigByPath[BASES_DEFAULT_TAB_PATH] ?? DEFAULT_BASE_CONFIG}
|
||||
onConfigChange={(cfg) => handleBaseConfigChange(BASES_DEFAULT_TAB_PATH, cfg)}
|
||||
isDefaultBase
|
||||
onSave={(name) => void handleBaseSave(BASES_DEFAULT_TAB_PATH, name)}
|
||||
externalSearch={externalBaseSearch}
|
||||
onExternalSearchConsumed={() => setExternalBaseSearch(undefined)}
|
||||
actions={{
|
||||
rename: knowledgeActions.rename,
|
||||
remove: knowledgeActions.remove,
|
||||
copyPath: knowledgeActions.copyPath,
|
||||
revealInFileManager: knowledgeActions.revealInFileManager,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
folderPath={knowledgeViewFolderPath}
|
||||
onNavigateFolder={(path) => { void navigateToView({ type: 'knowledge-view', folderPath: path ?? undefined }) }}
|
||||
onNavigateFolder={(path) => {
|
||||
setKnowledgeViewMode('files')
|
||||
void navigateToView({ type: 'knowledge-view', folderPath: path ?? undefined, mode: 'files' })
|
||||
}}
|
||||
onOpenNote={(path) => navigateToFile(path)}
|
||||
onOpenGraph={() => knowledgeActions.openGraph()}
|
||||
onOpenSearch={() => { setSearchDefaultScope('knowledge'); setIsSearchOpen(true) }}
|
||||
onOpenBases={() => knowledgeActions.openBases()}
|
||||
onVoiceNoteCreated={handleVoiceNoteCreated}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -5796,7 +5836,7 @@ function App() {
|
|||
config={baseConfigByPath[selectedPath] ?? DEFAULT_BASE_CONFIG}
|
||||
onConfigChange={(cfg) => handleBaseConfigChange(selectedPath, cfg)}
|
||||
isDefaultBase={selectedPath === BASES_DEFAULT_TAB_PATH}
|
||||
onSave={(name) => void handleBaseSave(name)}
|
||||
onSave={(name) => void handleBaseSave(selectedPath, name)}
|
||||
externalSearch={externalBaseSearch}
|
||||
onExternalSearchConsumed={() => setExternalBaseSearch(undefined)}
|
||||
actions={{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
||||
import {
|
||||
ArrowLeft,
|
||||
ChevronRight,
|
||||
|
|
@ -46,17 +46,21 @@ export type KnowledgeViewActions = {
|
|||
onOpenInNewTab?: (path: string) => void
|
||||
}
|
||||
|
||||
export type KnowledgeViewMode = 'graph' | 'basis' | 'files'
|
||||
|
||||
type KnowledgeViewProps = {
|
||||
tree: TreeNode[]
|
||||
actions: KnowledgeViewActions
|
||||
mode: KnowledgeViewMode
|
||||
onModeChange: (mode: KnowledgeViewMode) => void
|
||||
graphContent: ReactNode
|
||||
basisContent: ReactNode
|
||||
// Folder currently being browsed (null = root overview). Controlled by the
|
||||
// app so drill-down participates in the global back/forward history.
|
||||
folderPath: string | null
|
||||
onNavigateFolder: (path: string | null) => void
|
||||
onOpenNote: (path: string) => void
|
||||
onOpenGraph: () => void
|
||||
onOpenSearch: () => void
|
||||
onOpenBases: () => void
|
||||
onVoiceNoteCreated?: (path: string) => void
|
||||
}
|
||||
|
||||
|
|
@ -145,12 +149,14 @@ function displayName(node: TreeNode): string {
|
|||
export function KnowledgeView({
|
||||
tree,
|
||||
actions,
|
||||
mode,
|
||||
onModeChange,
|
||||
graphContent,
|
||||
basisContent,
|
||||
folderPath,
|
||||
onNavigateFolder,
|
||||
onOpenNote,
|
||||
onOpenGraph,
|
||||
onOpenSearch,
|
||||
onOpenBases,
|
||||
onVoiceNoteCreated,
|
||||
}: KnowledgeViewProps) {
|
||||
const [renameTarget, setRenameTarget] = useState<string | null>(null)
|
||||
|
|
@ -184,27 +190,46 @@ export function KnowledgeView({
|
|||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<div className="shrink-0 flex items-start justify-between gap-4 border-b border-border px-8 py-6">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Notes</h1>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Brain</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{totalNotes} {totalNotes === 1 ? 'note' : 'notes'} across {folders.length}{' '}
|
||||
{folders.length === 1 ? 'folder' : 'folders'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<div className="inline-flex overflow-hidden rounded-lg border border-border bg-background">
|
||||
<ViewModeButton
|
||||
icon={Network}
|
||||
label="Graph"
|
||||
active={mode === 'graph'}
|
||||
onClick={() => onModeChange('graph')}
|
||||
/>
|
||||
<ViewModeButton
|
||||
icon={Table2}
|
||||
label="Base"
|
||||
active={mode === 'basis'}
|
||||
onClick={() => onModeChange('basis')}
|
||||
/>
|
||||
<ViewModeButton
|
||||
icon={FileText}
|
||||
label="Files"
|
||||
active={mode === 'files'}
|
||||
onClick={() => onModeChange('files')}
|
||||
/>
|
||||
</div>
|
||||
<VoiceNoteButton onNoteCreated={onVoiceNoteCreated} />
|
||||
<SecondaryButton icon={SearchIcon} label="Search" onClick={onOpenSearch} />
|
||||
<SecondaryButton icon={Network} label="Graph" onClick={onOpenGraph} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => actions.createNote(currentFolder?.path)}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
<FilePlus className="size-4" />
|
||||
<span>New note</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mode === 'graph' ? (
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
{graphContent}
|
||||
</div>
|
||||
) : mode === 'basis' ? (
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
{basisContent}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="mx-auto w-full max-w-3xl px-8 py-6">
|
||||
{currentFolder ? (
|
||||
|
|
@ -267,11 +292,12 @@ export function KnowledgeView({
|
|||
<QuickActions
|
||||
actions={actions}
|
||||
currentFolder={currentFolder}
|
||||
onOpenBases={onOpenBases}
|
||||
onOpenSearch={onOpenSearch}
|
||||
onFolderCreated={setRenameTarget}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -279,12 +305,12 @@ export function KnowledgeView({
|
|||
function QuickActions({
|
||||
actions,
|
||||
currentFolder,
|
||||
onOpenBases,
|
||||
onOpenSearch,
|
||||
onFolderCreated,
|
||||
}: {
|
||||
actions: KnowledgeViewActions
|
||||
currentFolder: TreeNode | null
|
||||
onOpenBases: () => void
|
||||
onOpenSearch: () => void
|
||||
onFolderCreated: (path: string) => void
|
||||
}) {
|
||||
// Inside a folder these target that folder; at the root they target knowledge/.
|
||||
|
|
@ -294,6 +320,7 @@ function QuickActions({
|
|||
<SectionHeader label="Quick actions" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<QuickAction icon={FilePlus} label="New note" onClick={() => actions.createNote(parent)} />
|
||||
<QuickAction icon={SearchIcon} label="Search" onClick={onOpenSearch} />
|
||||
<QuickAction
|
||||
icon={FolderPlus}
|
||||
label="New folder"
|
||||
|
|
@ -304,7 +331,6 @@ function QuickActions({
|
|||
} catch { /* ignore */ }
|
||||
}}
|
||||
/>
|
||||
<QuickAction icon={Table2} label="Open as base" onClick={onOpenBases} />
|
||||
<QuickAction
|
||||
icon={FolderOpen}
|
||||
label={`Reveal in ${getFileManagerName()}`}
|
||||
|
|
@ -315,20 +341,26 @@ function QuickActions({
|
|||
)
|
||||
}
|
||||
|
||||
function SecondaryButton({
|
||||
function ViewModeButton({
|
||||
icon: Icon,
|
||||
label,
|
||||
active,
|
||||
onClick,
|
||||
}: {
|
||||
icon: typeof SearchIcon
|
||||
label: string
|
||||
active: boolean
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg border border-border bg-background px-3 py-1.5 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
aria-pressed={active}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 px-3 py-1.5 text-sm transition-colors',
|
||||
active ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent/60 hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
<span>{label}</span>
|
||||
|
|
@ -532,7 +564,7 @@ function FolderDetail({
|
|||
onClick={() => onNavigate(null)}
|
||||
className="rounded-md px-1.5 py-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
Notes
|
||||
Brain
|
||||
</button>
|
||||
{crumbs.map((c, i) => (
|
||||
<span key={c.path} className="flex min-w-0 items-center gap-1.5">
|
||||
|
|
|
|||
|
|
@ -819,7 +819,7 @@ export function SidebarContentPanel({
|
|||
>
|
||||
<FileText className="size-4 shrink-0" />
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="truncate">Knowledge</span>
|
||||
<span className="truncate">Brain</span>
|
||||
{knowledgeUpdatedLabel && (
|
||||
<span className="truncate text-[11px] text-muted-foreground">{knowledgeUpdatedLabel}</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,8 @@ Unlike other AI assistants that start cold every session, you have access to a l
|
|||
When a user asks you to prep them for a call with someone, you already know every prior decision, concerns they've raised, and commitments on both sides - because memory has been accumulating across every email and call, not reconstructed on demand.
|
||||
|
||||
## The Knowledge Graph
|
||||
The knowledge graph is the user's **Brain**. If the user says "my brain", "the brain", "look into your brain", "check my brain", "Brain", or similar, they mean the knowledge graph stored in \`knowledge/\`. Treat "Brain" and "knowledge graph" as the same thing.
|
||||
|
||||
The knowledge graph is stored as plain markdown with Obsidian-style backlinks in \`knowledge/\` (inside the workspace). The folder is organized into these categories:
|
||||
- **Notes/** - Default location for user-authored notes. Create new notes here unless the user specifies a different folder.
|
||||
- **People/** - Notes on individuals, tracking relationships, decisions, and commitments
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue