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:
co-authored by
Claude Opus 4.5
parent
57a20899a5
commit
c9d85998c1
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user