sync: Phase 3 Agent Features from private repo

Features synced:
- LucideIconPicker component (115 icons for dimension customization)
- Dimension editing modal (descriptions + icons)
- Compound filters (per-column AND filters)
- Improved dimension assignment with keyword tagging
- queryDimensions and getDimension agent tools

New files:
- src/components/common/LucideIconPicker.tsx
- src/tools/database/queryDimensions.ts
- src/tools/database/getDimension.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2025-12-25 10:05:02 +11:00
co-authored by Claude Opus 4.5
parent 57a20899a5
commit c9d85998c1
8 changed files with 1630 additions and 174 deletions
+150 -75
View File
@@ -46,39 +46,59 @@ export class DimensionService {
}
/**
* Automatically assign locked dimensions to a node based on its content
* Automatically assign locked dimensions + suggest keyword dimensions
* Returns { locked: string[], keywords: string[] }
*/
static async assignDimensions(nodeData: {
title: string;
content?: string;
link?: string;
}): Promise<{ locked: string[]; keywords: string[] }> {
try {
const lockedDimensions = await this.getLockedDimensions();
if (lockedDimensions.length === 0) {
console.log('[DimensionAssignment] No locked dimensions available');
return { locked: [], keywords: [] };
}
const prompt = this.buildAssignmentPrompt(nodeData, lockedDimensions);
console.log(`[DimensionAssignment] Processing: "${nodeData.title}"`);
const response = await generateText({
model: openaiProvider('gpt-4o-mini'),
prompt,
maxOutputTokens: 150, // Increased for two-part response
temperature: 0.1,
});
console.log(`[DimensionAssignment] AI Response:\n${response.text}`);
const result = this.parseAssignmentResponse(response.text, lockedDimensions);
console.log(`[DimensionAssignment] Locked: ${result.locked.join(', ') || 'none'}`);
console.log(`[DimensionAssignment] Keywords: ${result.keywords.join(', ') || 'none'}`);
return result;
} catch (error) {
console.error('[DimensionAssignment] Error:', error);
return { locked: [], keywords: [] };
}
}
/**
* Legacy method for backwards compatibility
* @deprecated Use assignDimensions() instead
*/
static async assignLockedDimensions(nodeData: {
title: string;
content?: string;
link?: string;
}): Promise<string[]> {
try {
const lockedDimensions = await this.getLockedDimensions();
if (lockedDimensions.length === 0) {
console.log('No locked dimensions available for assignment');
return [];
}
const prompt = this.buildAssignmentPrompt(nodeData, lockedDimensions);
const response = await generateText({
model: openaiProvider('gpt-4o-mini'),
prompt,
maxOutputTokens: 100,
temperature: 0.1, // Low temperature for consistent results
});
const assignedDimensions = this.parseAssignmentResponse(response.text, lockedDimensions);
console.log(`Assigned dimensions for "${nodeData.title}": ${assignedDimensions.join(', ')}`);
return assignedDimensions;
} catch (error) {
console.error('Error assigning dimensions:', error);
return []; // Graceful fallback
}
const result = await this.assignDimensions(nodeData);
return result.locked;
}
/**
@@ -122,80 +142,135 @@ export class DimensionService {
}
/**
* Build AI prompt for dimension assignment
* Build AI prompt for dimension assignment (locked + keyword dimensions)
*/
private static buildAssignmentPrompt(
nodeData: { title: string; content?: string; link?: string },
lockedDimensions: LockedDimension[]
): string {
const contentPreview = nodeData.content?.slice(0, 500) || '';
const dimensionsList = lockedDimensions
.map(d => `- ${d.name}: ${d.description || `Infer purpose from name: ${d.name}`}`)
.join('\n');
const contentPreview = nodeData.content?.slice(0, 1000) || '';
return `Analyze this node content and assign appropriate dimensions from the available locked dimensions.
// Only include dimensions that have descriptions
const dimensionsWithDescriptions = lockedDimensions.filter(d => d.description && d.description.trim().length > 0);
Node:
const dimensionsList = dimensionsWithDescriptions
.map(d => `DIMENSION: "${d.name}"\nDESCRIPTION: ${d.description}`)
.join('\n---\n');
return `You are categorizing a knowledge node. You will:
1. Assign LOCKED dimensions (from a provided list)
2. Suggest KEYWORD dimensions (to aid searchability)
=== NODE TO CATEGORIZE ===
Title: ${nodeData.title}
Content: ${contentPreview}${nodeData.content && nodeData.content.length > 500 ? '...' : ''}
Content: ${contentPreview}${nodeData.content && nodeData.content.length > 1000 ? '...' : ''}
URL: ${nodeData.link || 'none'}
Available Locked Dimensions:
=== PART 1: LOCKED DIMENSIONS ===
CRITICAL: Read each dimension's DESCRIPTION carefully.
The description defines what belongs in that dimension.
Only assign if the content CLEARLY matches the description.
If unsure, skip it — better to miss than assign incorrectly.
Maximum 3 locked dimensions.
AVAILABLE LOCKED DIMENSIONS:
${dimensionsList}
Return only the dimension names that best match this content, maximum 3 dimensions.
Respond with just the dimension names, one per line.
If no dimensions are appropriate, respond with "none".
=== PART 2: KEYWORD DIMENSIONS ===
Suggest 1-3 simple, lowercase keywords that:
- Capture the ESSENCE of this content
- Make it easily SEARCHABLE
- Are thoughtful and aid organization
Examples of good responses:
Work
Learning
Good keywords: ai, podcast, paper, productivity, machine-learning, book, video, interview, tutorial, framework
If unsure what keywords fit, respond with "none" — no noise is better than bad tags.
or
=== RESPONSE FORMAT ===
LOCKED:
[dimension names from the list above, one per line, or "none"]
Research
Ideas
Tech
or
none`;
KEYWORDS:
[lowercase keyword suggestions, one per line, or "none"]`;
}
/**
* Parse AI response and validate dimension names
* Parse AI response and extract locked + keyword dimensions
*/
private static parseAssignmentResponse(
response: string,
response: string,
availableDimensions: LockedDimension[]
): string[] {
const lines = response.trim().toLowerCase().split('\n');
const availableNames = availableDimensions.map(d => d.name.toLowerCase());
const validDimensions: string[] = [];
): { locked: string[]; keywords: string[] } {
const lockedDimensions: string[] = [];
const keywordDimensions: string[] = [];
for (const line of lines) {
const dimensionName = line.trim();
if (dimensionName === 'none') {
break;
}
// Find matching dimension (case-insensitive)
const matchedDimension = availableDimensions.find(
d => d.name.toLowerCase() === dimensionName
);
if (matchedDimension && !validDimensions.includes(matchedDimension.name)) {
validDimensions.push(matchedDimension.name);
// Limit to 3 dimensions max
if (validDimensions.length >= 3) {
break;
// Split response into LOCKED and KEYWORDS sections
const lockedMatch = response.match(/LOCKED:\s*([\s\S]*?)(?=KEYWORDS:|$)/i);
const keywordsMatch = response.match(/KEYWORDS:\s*([\s\S]*?)$/i);
// Parse LOCKED section
if (lockedMatch) {
const lockedLines = lockedMatch[1].trim().split('\n');
for (const line of lockedLines) {
const dimensionName = line.trim().toLowerCase();
if (dimensionName === 'none' || dimensionName === '') {
continue;
}
// Find matching dimension (case-insensitive)
const matchedDimension = availableDimensions.find(
d => d.name.toLowerCase() === dimensionName
);
if (matchedDimension && !lockedDimensions.includes(matchedDimension.name)) {
lockedDimensions.push(matchedDimension.name);
// Limit to 3 locked dimensions
if (lockedDimensions.length >= 3) {
break;
}
}
}
}
return validDimensions;
// Parse KEYWORDS section
if (keywordsMatch) {
const keywordLines = keywordsMatch[1].trim().split('\n');
for (const line of keywordLines) {
// Clean and normalize keyword
const keyword = line.trim().toLowerCase()
.replace(/[^a-z0-9-]/g, '') // Only allow lowercase letters, numbers, hyphens
.slice(0, 30); // Max 30 chars per keyword
if (keyword === 'none' || keyword === '' || keyword.length < 2) {
continue;
}
if (!keywordDimensions.includes(keyword)) {
keywordDimensions.push(keyword);
// Limit to 3 keywords
if (keywordDimensions.length >= 3) {
break;
}
}
}
}
return { locked: lockedDimensions, keywords: keywordDimensions };
}
/**
* Create or get a keyword dimension (unlocked)
*/
static async ensureKeywordDimension(keyword: string): Promise<void> {
const sqlite = getSQLiteClient();
// INSERT OR IGNORE - if dimension exists, do nothing
sqlite.query(`
INSERT OR IGNORE INTO dimensions(name, description, is_priority, updated_at)
VALUES (?, ?, 0, CURRENT_TIMESTAMP)
`, [keyword, null]);
}
}