Scheduled research persona generation
This commit is contained in:
191
frontend/src/utils/keywordExpansion.ts
Normal file
191
frontend/src/utils/keywordExpansion.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Smart Keyword Expansion Utility
|
||||
* Expands user keywords with industry-specific related terms using rule-based logic
|
||||
*/
|
||||
|
||||
// Industry-specific keyword expansion maps
|
||||
// Format: { industry: { keyword: [expansions] } }
|
||||
const industryKeywordExpansions: Record<string, Record<string, string[]>> = {
|
||||
Healthcare: {
|
||||
'AI': ['medical AI', 'healthcare AI', 'clinical AI', 'diagnostic AI', 'healthcare automation'],
|
||||
'tools': ['medical devices', 'clinical tools', 'diagnostic systems', 'healthcare software'],
|
||||
'automation': ['healthcare automation', 'clinical automation', 'patient care automation', 'medical workflow automation'],
|
||||
'technology': ['healthtech', 'medical technology', 'clinical technology', 'digital health'],
|
||||
'data': ['health data', 'medical records', 'patient data', 'clinical data', 'healthcare analytics'],
|
||||
'research': ['medical research', 'clinical research', 'biomedical research', 'healthcare studies'],
|
||||
'management': ['patient management', 'care coordination', 'healthcare administration'],
|
||||
},
|
||||
Technology: {
|
||||
'AI': ['machine learning', 'deep learning', 'neural networks', 'artificial intelligence applications'],
|
||||
'cloud': ['AWS', 'Azure', 'GCP', 'cloud infrastructure', 'cloud computing'],
|
||||
'security': ['cybersecurity', 'data protection', 'privacy compliance', 'information security'],
|
||||
'automation': ['IT automation', 'devops automation', 'software automation', 'process automation'],
|
||||
'development': ['software development', 'web development', 'mobile development', 'app development'],
|
||||
'tools': ['development tools', 'software tools', 'developer tools', 'tech stack'],
|
||||
'platform': ['SaaS platform', 'cloud platform', 'development platform', 'tech platform'],
|
||||
},
|
||||
Finance: {
|
||||
'fintech': ['financial technology', 'digital banking', 'payment solutions', 'financial services tech'],
|
||||
'investing': ['investment strategies', 'portfolio management', 'trading platforms', 'wealth management'],
|
||||
'cryptocurrency': ['blockchain', 'digital assets', 'DeFi', 'crypto trading'],
|
||||
'banking': ['digital banking', 'online banking', 'mobile banking', 'banking technology'],
|
||||
'payment': ['payment processing', 'payment gateways', 'digital payments', 'payment solutions'],
|
||||
'analysis': ['financial analysis', 'market analysis', 'risk analysis', 'investment analysis'],
|
||||
'compliance': ['financial compliance', 'regulatory compliance', 'fintech regulations'],
|
||||
},
|
||||
Marketing: {
|
||||
'SEO': ['search engine optimization', 'SEO strategy', 'SEO tools', 'keyword research'],
|
||||
'content': ['content marketing', 'content strategy', 'content creation', 'content distribution'],
|
||||
'social media': ['social media marketing', 'social media strategy', 'social media advertising'],
|
||||
'advertising': ['digital advertising', 'online advertising', 'PPC', 'display advertising'],
|
||||
'analytics': ['marketing analytics', 'web analytics', 'campaign analytics', 'performance metrics'],
|
||||
'automation': ['marketing automation', 'email marketing', 'lead generation', 'CRM'],
|
||||
'strategy': ['marketing strategy', 'brand strategy', 'digital strategy', 'growth strategy'],
|
||||
},
|
||||
Business: {
|
||||
'management': ['business management', 'operations management', 'strategic management'],
|
||||
'strategy': ['business strategy', 'growth strategy', 'competitive strategy', 'market strategy'],
|
||||
'startup': ['startup funding', 'venture capital', 'startup ecosystem', 'entrepreneurship'],
|
||||
'operations': ['business operations', 'process optimization', 'operational efficiency'],
|
||||
'leadership': ['business leadership', 'executive leadership', 'management leadership'],
|
||||
'innovation': ['business innovation', 'digital transformation', 'business disruption'],
|
||||
'analytics': ['business analytics', 'data analytics', 'business intelligence', 'KPIs'],
|
||||
},
|
||||
Education: {
|
||||
'e-learning': ['online learning', 'distance education', 'digital learning', 'virtual classrooms'],
|
||||
'edtech': ['education technology', 'learning management systems', 'educational software'],
|
||||
'teaching': ['teaching methods', 'pedagogy', 'instructional design', 'curriculum development'],
|
||||
'student': ['student engagement', 'student success', 'student analytics', 'learning outcomes'],
|
||||
'training': ['professional training', 'skills development', 'corporate training', 'certification'],
|
||||
'assessment': ['educational assessment', 'learning assessment', 'student evaluation'],
|
||||
},
|
||||
Real_Estate: {
|
||||
'property': ['real estate', 'real estate market', 'property investment', 'property management'],
|
||||
'technology': ['proptech', 'real estate technology', 'property tech', 'real estate software'],
|
||||
'investment': ['real estate investment', 'property investment', 'real estate portfolio'],
|
||||
'market': ['housing market', 'real estate trends', 'market analysis', 'property values'],
|
||||
'management': ['property management', 'facility management', 'real estate operations'],
|
||||
},
|
||||
Travel: {
|
||||
'tourism': ['travel industry', 'hospitality', 'travel trends', 'tourism technology'],
|
||||
'booking': ['travel booking', 'online booking', 'travel platforms', 'reservation systems'],
|
||||
'technology': ['travel tech', 'travel technology', 'tourism tech', 'hospitality technology'],
|
||||
'experience': ['travel experience', 'customer experience', 'tourism experiences'],
|
||||
},
|
||||
Science: {
|
||||
'research': ['scientific research', 'academic research', 'research methods', 'research publications'],
|
||||
'technology': ['scientific technology', 'laboratory technology', 'research tools'],
|
||||
'data': ['scientific data', 'research data', 'experimental data', 'data analysis'],
|
||||
'innovation': ['scientific innovation', 'research innovation', 'scientific breakthroughs'],
|
||||
},
|
||||
Legal: {
|
||||
'technology': ['legal tech', 'legal technology', 'law tech', 'legal software'],
|
||||
'compliance': ['legal compliance', 'regulatory compliance', 'legal requirements'],
|
||||
'automation': ['legal automation', 'document automation', 'legal process automation'],
|
||||
'research': ['legal research', 'case research', 'legal analysis'],
|
||||
},
|
||||
Manufacturing: {
|
||||
'automation': ['manufacturing automation', 'industrial automation', 'factory automation', 'production automation'],
|
||||
'technology': ['industrial technology', 'manufacturing tech', 'Industry 4.0', 'smart manufacturing'],
|
||||
'quality': ['quality control', 'quality assurance', 'quality management', 'quality standards'],
|
||||
'efficiency': ['manufacturing efficiency', 'production efficiency', 'operational efficiency'],
|
||||
},
|
||||
Retail: {
|
||||
'e-commerce': ['online retail', 'digital commerce', 'ecommerce platform', 'online shopping'],
|
||||
'technology': ['retail tech', 'retail technology', 'retail innovation', 'retail software'],
|
||||
'customer': ['customer experience', 'customer engagement', 'customer service', 'customer analytics'],
|
||||
'inventory': ['inventory management', 'stock management', 'supply chain', 'warehouse management'],
|
||||
},
|
||||
Energy: {
|
||||
'renewable': ['solar energy', 'wind energy', 'renewable technology', 'clean energy'],
|
||||
'technology': ['energy technology', 'energy innovation', 'energy management systems'],
|
||||
'efficiency': ['energy efficiency', 'energy optimization', 'energy conservation'],
|
||||
},
|
||||
Agriculture: {
|
||||
'technology': ['agtech', 'agricultural technology', 'farm technology', 'precision agriculture'],
|
||||
'automation': ['farm automation', 'agricultural automation', 'precision farming'],
|
||||
'sustainability': ['sustainable farming', 'organic farming', 'agricultural sustainability'],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Expands keywords based on industry context
|
||||
* @param keywords - Array of user-entered keywords
|
||||
* @param industry - Selected industry (or 'General')
|
||||
* @returns Array of expanded keywords (originals + suggestions)
|
||||
*/
|
||||
export function expandKeywords(keywords: string[], industry: string): {
|
||||
original: string[];
|
||||
expanded: string[];
|
||||
suggestions: string[];
|
||||
} {
|
||||
if (!keywords || keywords.length === 0) {
|
||||
return { original: [], expanded: [], suggestions: [] };
|
||||
}
|
||||
|
||||
// Normalize industry name (handle spaces and case)
|
||||
const normalizedIndustry = industry.replace(/\s+/g, '_');
|
||||
|
||||
// Get expansion map for this industry, or empty object if not found
|
||||
const expansionMap = industryKeywordExpansions[normalizedIndustry] || {};
|
||||
|
||||
const originalKeywords = [...keywords];
|
||||
const suggestions: string[] = [];
|
||||
const expandedSet = new Set<string>();
|
||||
|
||||
// Add original keywords to expanded set
|
||||
originalKeywords.forEach(k => expandedSet.add(k.toLowerCase().trim()));
|
||||
|
||||
// For each keyword, find expansions
|
||||
originalKeywords.forEach(keyword => {
|
||||
const keywordLower = keyword.toLowerCase().trim();
|
||||
|
||||
// Direct match in expansion map
|
||||
if (expansionMap[keywordLower]) {
|
||||
expansionMap[keywordLower].forEach(expansion => {
|
||||
if (!expandedSet.has(expansion.toLowerCase())) {
|
||||
suggestions.push(expansion);
|
||||
expandedSet.add(expansion.toLowerCase());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Partial match: check if keyword contains any expansion key
|
||||
Object.keys(expansionMap).forEach(expansionKey => {
|
||||
if (keywordLower.includes(expansionKey) || expansionKey.includes(keywordLower)) {
|
||||
expansionMap[expansionKey].forEach(expansion => {
|
||||
if (!expandedSet.has(expansion.toLowerCase())) {
|
||||
suggestions.push(expansion);
|
||||
expandedSet.add(expansion.toLowerCase());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Return structure
|
||||
return {
|
||||
original: originalKeywords,
|
||||
expanded: Array.from(expandedSet).map(k => {
|
||||
// Preserve original casing if it exists in originals
|
||||
const originalMatch = originalKeywords.find(ok => ok.toLowerCase() === k);
|
||||
return originalMatch || k;
|
||||
}),
|
||||
suggestions: suggestions.slice(0, 8), // Limit to 8 suggestions to avoid overwhelming UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats keyword for display (capitalize first letter)
|
||||
*/
|
||||
export function formatKeyword(keyword: string): string {
|
||||
if (!keyword) return keyword;
|
||||
return keyword.charAt(0).toUpperCase() + keyword.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a keyword is an original (user-entered) or a suggestion
|
||||
*/
|
||||
export function isOriginalKeyword(keyword: string, originalKeywords: string[]): boolean {
|
||||
return originalKeywords.some(ok => ok.toLowerCase() === keyword.toLowerCase());
|
||||
}
|
||||
193
frontend/src/utils/researchAngles.ts
Normal file
193
frontend/src/utils/researchAngles.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Alternative Research Angles Utility
|
||||
* Generates related research angles based on user query intent using rule-based patterns
|
||||
*/
|
||||
|
||||
// Pattern-based angle generation templates
|
||||
const anglePatterns: Record<string, string[]> = {
|
||||
tools: [
|
||||
'Compare {topic}',
|
||||
'{topic} ROI analysis',
|
||||
'Best {topic} for {industry}',
|
||||
'{topic} implementation guide',
|
||||
'Top {topic} features and pricing',
|
||||
],
|
||||
trends: [
|
||||
'Latest {topic} trends',
|
||||
'{topic} market analysis',
|
||||
'{topic} future predictions',
|
||||
'{topic} adoption rates',
|
||||
'Emerging {topic} technologies',
|
||||
],
|
||||
strategies: [
|
||||
'{topic} implementation guide',
|
||||
'{topic} best practices',
|
||||
'{topic} case studies',
|
||||
'{topic} success strategies',
|
||||
'{topic} optimization techniques',
|
||||
],
|
||||
analysis: [
|
||||
'{topic} competitive analysis',
|
||||
'{topic} market share',
|
||||
'{topic} industry leaders',
|
||||
'{topic} SWOT analysis',
|
||||
'{topic} benchmarking',
|
||||
],
|
||||
guides: [
|
||||
'{topic} getting started guide',
|
||||
'{topic} for beginners',
|
||||
'{topic} step-by-step tutorial',
|
||||
'{topic} troubleshooting',
|
||||
'{topic} expert tips',
|
||||
],
|
||||
comparison: [
|
||||
'{topic} vs alternatives',
|
||||
'Best {topic} comparison',
|
||||
'{topic} feature comparison',
|
||||
'{topic} pricing comparison',
|
||||
'{topic} pros and cons',
|
||||
],
|
||||
general: [
|
||||
'What is {topic}',
|
||||
'How {topic} works',
|
||||
'{topic} benefits and challenges',
|
||||
'{topic} industry insights',
|
||||
'{topic} expert opinions',
|
||||
],
|
||||
};
|
||||
|
||||
// Keywords that indicate query intent
|
||||
const intentKeywords: Record<string, string[]> = {
|
||||
tools: ['tools', 'software', 'platform', 'system', 'solution', 'app', 'application', 'toolkit', 'suite'],
|
||||
trends: ['trends', 'future', 'emerging', 'latest', 'new', 'innovation', 'development', 'growth'],
|
||||
strategies: ['strategy', 'plan', 'approach', 'method', 'best practices', 'how to', 'guide', 'implementation'],
|
||||
analysis: ['analysis', 'compare', 'review', 'evaluate', 'assessment', 'study', 'research'],
|
||||
guides: ['guide', 'tutorial', 'how to', 'getting started', 'learn', 'tips', 'advice'],
|
||||
comparison: ['vs', 'versus', 'compare', 'comparison', 'alternative', 'difference'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Detects the primary intent of a query
|
||||
*/
|
||||
function detectQueryIntent(query: string): string {
|
||||
const queryLower = query.toLowerCase();
|
||||
|
||||
// Check each intent category
|
||||
for (const [intent, keywords] of Object.entries(intentKeywords)) {
|
||||
if (keywords.some(keyword => queryLower.includes(keyword))) {
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
|
||||
return 'general';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the main topic from a query
|
||||
*/
|
||||
function extractTopic(query: string, industry: string): string {
|
||||
// Remove common intent words to get the core topic
|
||||
const intentWords = Object.values(intentKeywords).flat();
|
||||
let topic = query.toLowerCase();
|
||||
|
||||
// Remove intent keywords
|
||||
for (const word of intentWords) {
|
||||
const regex = new RegExp(`\\b${word}\\b`, 'gi');
|
||||
topic = topic.replace(regex, '').trim();
|
||||
}
|
||||
|
||||
// Clean up extra whitespace and common stop words
|
||||
topic = topic
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\b(a|an|the|in|on|at|for|with|to|of|and|or|but)\b/g, '')
|
||||
.trim();
|
||||
|
||||
// If topic is too short or empty, use original query
|
||||
if (topic.length < 3 || topic.split(' ').length === 0) {
|
||||
topic = query.toLowerCase();
|
||||
}
|
||||
|
||||
// Capitalize first letter
|
||||
return topic.charAt(0).toUpperCase() + topic.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates alternative research angles based on user query
|
||||
* @param query - User's research query/keywords
|
||||
* @param industry - Selected industry (optional)
|
||||
* @returns Array of alternative research angle suggestions
|
||||
*/
|
||||
export function generateResearchAngles(query: string, industry: string = 'General'): string[] {
|
||||
if (!query || query.trim().length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Detect primary intent
|
||||
const intent = detectQueryIntent(query);
|
||||
|
||||
// Extract main topic
|
||||
const topic = extractTopic(query, industry);
|
||||
|
||||
// Get patterns for detected intent (fallback to general)
|
||||
const patterns = anglePatterns[intent] || anglePatterns.general;
|
||||
|
||||
// Generate angles using patterns
|
||||
const angles: string[] = [];
|
||||
|
||||
for (const pattern of patterns.slice(0, 5)) { // Limit to 5 angles
|
||||
let angle = pattern.replace('{topic}', topic);
|
||||
|
||||
// Replace industry placeholder if present
|
||||
if (industry && industry !== 'General') {
|
||||
angle = angle.replace('{industry}', industry);
|
||||
} else {
|
||||
// Remove industry-specific placeholder if no industry
|
||||
angle = angle.replace(' for {industry}', '');
|
||||
}
|
||||
|
||||
// Capitalize first letter
|
||||
angle = angle.charAt(0).toUpperCase() + angle.slice(1);
|
||||
|
||||
// Skip if angle is too similar to original query
|
||||
const angleLower = angle.toLowerCase();
|
||||
const queryLower = query.toLowerCase();
|
||||
|
||||
if (angleLower !== queryLower && !queryLower.includes(angleLower) && !angleLower.includes(queryLower)) {
|
||||
angles.push(angle);
|
||||
}
|
||||
}
|
||||
|
||||
// Add industry-specific angle if industry is set
|
||||
if (industry && industry !== 'General' && angles.length < 5) {
|
||||
const industryAngle = `${topic} in ${industry} industry`;
|
||||
if (!angles.some(a => a.toLowerCase() === industryAngle.toLowerCase())) {
|
||||
angles.push(industryAngle);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have fewer than 3 angles, add some general ones
|
||||
if (angles.length < 3) {
|
||||
const generalPatterns = anglePatterns.general.slice(0, 3 - angles.length);
|
||||
for (const pattern of generalPatterns) {
|
||||
const angle = pattern.replace('{topic}', topic);
|
||||
if (!angles.some(a => a.toLowerCase() === angle.toLowerCase())) {
|
||||
angles.push(angle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates and limit to 5
|
||||
const uniqueAngles = Array.from(new Set(angles.map(a => a.toLowerCase())))
|
||||
.slice(0, 5)
|
||||
.map(a => a.charAt(0).toUpperCase() + a.slice(1));
|
||||
|
||||
return uniqueAngles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an angle for display
|
||||
*/
|
||||
export function formatAngle(angle: string): string {
|
||||
if (!angle) return angle;
|
||||
return angle.charAt(0).toUpperCase() + angle.slice(1);
|
||||
}
|
||||
132
frontend/src/utils/researchHistory.ts
Normal file
132
frontend/src/utils/researchHistory.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { ResearchMode } from '../services/blogWriterApi';
|
||||
|
||||
export interface ResearchHistoryEntry {
|
||||
keywords: string[];
|
||||
industry: string;
|
||||
targetAudience: string;
|
||||
researchMode: ResearchMode;
|
||||
timestamp: number;
|
||||
resultSummary?: string; // Optional: show snippet from results
|
||||
}
|
||||
|
||||
const HISTORY_STORAGE_KEY = 'alwrity_research_history';
|
||||
const MAX_HISTORY_ENTRIES = 5;
|
||||
|
||||
/**
|
||||
* Get all research history entries, sorted by most recent first
|
||||
*/
|
||||
export function getResearchHistory(): ResearchHistoryEntry[] {
|
||||
try {
|
||||
const stored = localStorage.getItem(HISTORY_STORAGE_KEY);
|
||||
if (!stored) return [];
|
||||
|
||||
const entries = JSON.parse(stored) as ResearchHistoryEntry[];
|
||||
if (!Array.isArray(entries)) return [];
|
||||
|
||||
// Sort by timestamp (most recent first) and limit to MAX_HISTORY_ENTRIES
|
||||
return entries
|
||||
.sort((a, b) => b.timestamp - a.timestamp)
|
||||
.slice(0, MAX_HISTORY_ENTRIES);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load research history:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new research entry to history
|
||||
*/
|
||||
export function addResearchHistory(entry: Omit<ResearchHistoryEntry, 'timestamp'>): void {
|
||||
try {
|
||||
const currentHistory = getResearchHistory();
|
||||
|
||||
// Create new entry with timestamp
|
||||
const newEntry: ResearchHistoryEntry = {
|
||||
...entry,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Check if similar entry already exists (same keywords, industry, audience)
|
||||
const existingIndex = currentHistory.findIndex(
|
||||
(e) =>
|
||||
JSON.stringify(e.keywords.sort()) === JSON.stringify(entry.keywords.sort()) &&
|
||||
e.industry === entry.industry &&
|
||||
e.targetAudience === entry.targetAudience &&
|
||||
e.researchMode === entry.researchMode
|
||||
);
|
||||
|
||||
// If exists, remove it (we'll add it back at the top)
|
||||
const updatedHistory =
|
||||
existingIndex >= 0
|
||||
? currentHistory.filter((_, i) => i !== existingIndex)
|
||||
: currentHistory;
|
||||
|
||||
// Add new entry at the beginning and limit to MAX_HISTORY_ENTRIES
|
||||
const finalHistory = [newEntry, ...updatedHistory].slice(0, MAX_HISTORY_ENTRIES);
|
||||
|
||||
localStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(finalHistory));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save research history:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all research history
|
||||
*/
|
||||
export function clearResearchHistory(): void {
|
||||
try {
|
||||
localStorage.removeItem(HISTORY_STORAGE_KEY);
|
||||
} catch (error) {
|
||||
console.warn('Failed to clear research history:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific entry from history by timestamp
|
||||
*/
|
||||
export function removeResearchHistoryEntry(timestamp: number): void {
|
||||
try {
|
||||
const currentHistory = getResearchHistory();
|
||||
const updatedHistory = currentHistory.filter((e) => e.timestamp !== timestamp);
|
||||
localStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(updatedHistory));
|
||||
} catch (error) {
|
||||
console.warn('Failed to remove research history entry:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp for display (e.g., "2 hours ago", "Yesterday")
|
||||
*/
|
||||
export function formatHistoryTimestamp(timestamp: number): string {
|
||||
const now = Date.now();
|
||||
const diffMs = now - timestamp;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'Just now';
|
||||
if (diffMins < 60) return `${diffMins} min${diffMins > 1 ? 's' : ''} ago`;
|
||||
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
||||
if (diffDays === 1) return 'Yesterday';
|
||||
if (diffDays < 7) return `${diffDays} days ago`;
|
||||
|
||||
// For older entries, show date
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a short summary from keywords for display
|
||||
*/
|
||||
export function getHistorySummary(entry: ResearchHistoryEntry): string {
|
||||
if (entry.resultSummary) {
|
||||
return entry.resultSummary.length > 60
|
||||
? entry.resultSummary.substring(0, 60) + '...'
|
||||
: entry.resultSummary;
|
||||
}
|
||||
|
||||
// Fallback to first keyword or keywords joined
|
||||
if (entry.keywords.length === 0) return 'Research query';
|
||||
if (entry.keywords.length === 1) return entry.keywords[0];
|
||||
return entry.keywords.slice(0, 2).join(', ') + (entry.keywords.length > 2 ? '...' : '');
|
||||
}
|
||||
Reference in New Issue
Block a user