Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/components/studio/right-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useStudioStore, useFilteredEntities } from '@/lib/studio/store'
import { ENTITY_TYPE_META } from '@/lib/studio/types'
import { search, type SearchResult } from '@/lib/search/retrieval'
import { buildEntityIndex } from '@/lib/studio/graph-index'
import { Search, X, Sparkles, FileText, Quote, ArrowRight } from 'lucide-react'
import { useMemo, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
Expand Down Expand Up @@ -39,6 +40,7 @@ function SearchPanel({ onCreateEntity }: { onCreateEntity?: (name: string) => vo
const startEdit = useStudioStore((s) => s.startEdit)
const [mode, setMode] = useState<'keyword' | 'ranked'>('keyword')
const filtered = useFilteredEntities()
const entityIndex = useMemo(() => buildEntityIndex(entities), [entities])
const rankedResults = useMemo(
() => (mode === 'ranked' ? search(entities, claims, searchQuery) : []),
[mode, entities, claims, searchQuery],
Expand Down Expand Up @@ -112,7 +114,7 @@ function SearchPanel({ onCreateEntity }: { onCreateEntity?: (name: string) => vo
<ul className="space-y-1.5" role="list" aria-label="Ranked search results">
{rankedResults.map((r: SearchResult) => {
const targetId = r.type === 'entity' ? r.id : r.entityId
const resolvedEntity = targetId ? entities.find((e) => e.id === targetId) : undefined
const resolvedEntity = targetId ? entityIndex.get(targetId) : undefined
const meta = resolvedEntity ? ENTITY_TYPE_META[resolvedEntity.type] : undefined
return (
<li key={r.id}>
Expand Down Expand Up @@ -187,7 +189,8 @@ function InspectorPanel() {
const deleteEntity = useStudioStore((s) => s.deleteEntity)
const selectEntity = useStudioStore((s) => s.selectEntity)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const entity = entities.find((e) => e.id === selectedEntityId) || entities[0]
const entityIndex = useMemo(() => buildEntityIndex(entities), [entities])
const entity = (selectedEntityId ? entityIndex.get(selectedEntityId) : undefined) || entities[0]
const deleteCancelRef = useRef<HTMLButtonElement>(null)

if (!entity) {
Expand Down Expand Up @@ -249,7 +252,7 @@ function InspectorPanel() {
</h4>
<ul className="space-y-1">
{entity.links.map((l, i) => {
const target = entities.find((e) => e.id === l.targetId)
const target = entityIndex.get(l.targetId)
if (!target) return null
return (
<li key={i}>
Expand Down
52 changes: 37 additions & 15 deletions src/lib/ai/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,19 @@ import { describe, it, expect } from 'vitest'
import { buildSystemPrompt } from './context'
import type { Entity, Claim } from '@/lib/studio/types'

function makeEntity(overrides: Partial<Entity> = {}): Entity {
return {
id: '1',
name: 'Test Entity',
type: 'concept',
description: 'A test entity for unit testing',
content: '',
tags: ['test'],
claims: [],
links: [],
createdAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-01T00:00:00Z',
...overrides,
}
}
const makeEntity = (overrides: Partial<Entity> = {}): Entity => ({
id: '1',
name: 'Test Entity',
type: 'concept',
description: 'A test entity for unit testing',
content: '',
tags: ['test'],
claims: [],
links: [],
createdAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-01T00:00:00Z',
...overrides,
})

const NO_CLAIMS: Claim[] = []

Expand All @@ -41,4 +39,28 @@ describe('buildSystemPrompt', () => {
const prompt = buildSystemPrompt('TypeScript', entities, NO_CLAIMS, true)
expect(prompt).toContain('Relevant entities from your library')
})

it('includes entity tags in prompt for matched entity results', () => {
const entities = [
makeEntity({ id: 'entity-ts', name: 'TypeScript Language', description: 'Strongly typed programming language', tags: ['javascript', 'compiler'] }),
]
const prompt = buildSystemPrompt('TypeScript', entities, NO_CLAIMS, true)
expect(prompt).toContain('[javascript, compiler]')
})

it('performs indexed entity lookup rapidly for large entity corpora', () => {
const largeCorpus = Array.from({ length: 500 }, (_, i) =>
makeEntity({
id: `entity-${i}`,
name: `Knowledge Topic ${i}`,
description: `Detailed description for knowledge topic item number ${i} with searchable terms.`,
tags: [`tag-${i % 10}`],
}),
)
const start = performance.now()
const prompt = buildSystemPrompt('Topic 250', largeCorpus, NO_CLAIMS, true)
const elapsed = performance.now() - start
expect(prompt).toContain('Topic 250')
expect(elapsed).toBeLessThan(50)
})
})
8 changes: 5 additions & 3 deletions src/lib/ai/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Entity, Claim } from '@/lib/studio/types'
import type { ChatMessage } from './types'
import { search } from '@/lib/search/retrieval'
import { buildEntityIndex } from '@/lib/studio/graph-index'
import type { ResearchResult } from './research'
import { buildResearchContext } from './research'

Expand All @@ -21,10 +22,11 @@ export function buildSystemPrompt(
if (augmentWithLocal && entities.length > 0) {
const results = search(entities, claims, query, 5)
if (results.length > 0) {
const entityIndex = buildEntityIndex(entities)
const contextParts = results.map((r) => {
const tags: string[] = []
const entity = entities.find((e) => e.id === r.entityId)
if (entity && entity.tags.length > 0) tags.push(...entity.tags)
const targetId = r.entityId ?? r.id
const entity = entityIndex.get(targetId)
const tags = entity?.tags ?? []
const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : ''
const desc = r.snippet ? `: ${r.snippet.slice(0, 200)}` : ''
return `- ${r.name}${tagStr}${desc}`
Expand Down
Loading