feat: Add user likes tab and local timeline, update branding assets
- Add likes tab to user profiles displaying user's liked posts - Implement new `/api/users/[handle]/likes` endpoint to fetch user's liked posts - Add local timeline feed type to show only local node posts without fediverse content - Update favicon and add new logotext branding asset - Enhance search results with isLiked and isReposted status for authenticated users - Populate like and repost information in search posts endpoint - Update profile page tabs to conditionally show likes tab for non-bot users - Add loading states and empty state messaging for likes tab - Remove deprecated guide page - Improve bot settings page and content generation logic
This commit is contained in:
@@ -108,6 +108,11 @@ export const MAX_SOURCE_CONTENT_LENGTH = 4000;
|
||||
*/
|
||||
export const MAX_CONVERSATION_CONTEXT_LENGTH = 2000;
|
||||
|
||||
/**
|
||||
* Maximum character length for previous posts context.
|
||||
*/
|
||||
export const MAX_PREVIOUS_POSTS_CONTEXT_LENGTH = 8000;
|
||||
|
||||
/**
|
||||
* Truncation suffix added to truncated content.
|
||||
*/
|
||||
@@ -212,18 +217,21 @@ export function isContentTruncated(content: string): boolean {
|
||||
* Includes personality context and instructions.
|
||||
*
|
||||
* @param personality - Bot's personality configuration
|
||||
* @param hasSourceContent - Whether the post has source content with a URL
|
||||
* @returns System prompt string
|
||||
*
|
||||
* Validates: Requirements 3.2, 11.1
|
||||
*/
|
||||
export function buildPostSystemPrompt(personality: PersonalityConfig): string {
|
||||
export function buildPostSystemPrompt(personality: PersonalityConfig, hasSourceContent: boolean = true): string {
|
||||
let prompt = personality.systemPrompt;
|
||||
|
||||
if (personality.responseStyle) {
|
||||
prompt += `\n\nResponse Style: ${personality.responseStyle}`;
|
||||
}
|
||||
|
||||
prompt += `\n\nIMPORTANT: Your posts MUST be under 450 characters (not including the URL). This leaves room for the source link. This is a strict limit.
|
||||
if (hasSourceContent) {
|
||||
// Instructions for posts with source content (include URL)
|
||||
prompt += `\n\nIMPORTANT: Your posts MUST be under 450 characters (not including the URL). This leaves room for the source link. This is a strict limit.
|
||||
|
||||
Instructions for creating posts:
|
||||
- Create engaging, original content based on the source material
|
||||
@@ -233,7 +241,22 @@ Instructions for creating posts:
|
||||
- Do not simply copy or summarize - add value with your unique voice
|
||||
- Do NOT use hashtags
|
||||
- Write like a human, not a marketing bot
|
||||
- AVOID repeating themes, phrases, or sentence structures from your previous posts
|
||||
- Format: [Your commentary] [URL]`;
|
||||
} else {
|
||||
// Instructions for original posts without source content (no URL needed)
|
||||
prompt += `\n\nIMPORTANT: Your posts MUST be under 500 characters. This is a strict limit.
|
||||
|
||||
Instructions for creating posts:
|
||||
- Create engaging, original content that fits your personality
|
||||
- Keep the text concise - aim for 100-400 characters
|
||||
- Do NOT include any URLs or links
|
||||
- Do NOT use hashtags
|
||||
- Write like a human, not a marketing bot
|
||||
- Be creative and stay in character
|
||||
- AVOID repeating themes, phrases, or sentence structures from your previous posts
|
||||
- Each post should feel fresh and different while maintaining your voice`;
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
@@ -294,22 +317,48 @@ Respond with a JSON object containing:
|
||||
*
|
||||
* @param sourceContent - Source content to post about
|
||||
* @param context - Optional additional context
|
||||
* @param previousPosts - Optional array of previous post contents for context
|
||||
* @returns User message string
|
||||
*/
|
||||
export function buildPostUserMessage(
|
||||
sourceContent?: ContentItem,
|
||||
context?: string
|
||||
context?: string,
|
||||
previousPosts?: string[]
|
||||
): string {
|
||||
let message = '';
|
||||
|
||||
// Add previous posts context if available
|
||||
if (previousPosts && previousPosts.length > 0) {
|
||||
message += 'Your recent posts (for context - avoid repeating similar content):\n';
|
||||
|
||||
// Truncate previous posts if too long
|
||||
let contextLength = 0;
|
||||
const relevantPosts: string[] = [];
|
||||
|
||||
for (const post of previousPosts) {
|
||||
if (contextLength + post.length > MAX_PREVIOUS_POSTS_CONTEXT_LENGTH) {
|
||||
break;
|
||||
}
|
||||
relevantPosts.push(`- ${post}`);
|
||||
contextLength += post.length;
|
||||
}
|
||||
|
||||
message += relevantPosts.join('\n');
|
||||
message += '\n\n---\n\n';
|
||||
}
|
||||
|
||||
if (!sourceContent) {
|
||||
if (context) {
|
||||
return `Create a post about the following:\n\n${context}`;
|
||||
message += `Create a post about the following:\n\n${context}`;
|
||||
} else {
|
||||
message += 'Create an engaging post for your followers.';
|
||||
}
|
||||
return 'Create an engaging post for your followers.';
|
||||
return message;
|
||||
}
|
||||
|
||||
const truncatedContent = truncateContent(sourceContent.content || '');
|
||||
|
||||
let message = `Create a post about the following content:\n\n`;
|
||||
message += `Create a post about the following content:\n\n`;
|
||||
message += `Title: ${sourceContent.title}\n`;
|
||||
message += `URL: ${sourceContent.url}\n`;
|
||||
|
||||
@@ -426,16 +475,19 @@ export class ContentGenerator {
|
||||
*
|
||||
* @param sourceContent - Optional source content to post about
|
||||
* @param context - Optional additional context
|
||||
* @param previousPosts - Optional array of previous post contents for context
|
||||
* @returns Generated content with token usage
|
||||
*
|
||||
* Validates: Requirements 3.2, 11.1, 11.2, 11.3
|
||||
*/
|
||||
async generatePost(
|
||||
sourceContent?: ContentItem,
|
||||
context?: string
|
||||
context?: string,
|
||||
previousPosts?: string[]
|
||||
): Promise<GeneratedContent> {
|
||||
const systemPrompt = buildPostSystemPrompt(this.bot.personalityConfig);
|
||||
const userMessage = buildPostUserMessage(sourceContent, context);
|
||||
const hasSourceContent = !!sourceContent;
|
||||
const systemPrompt = buildPostSystemPrompt(this.bot.personalityConfig, hasSourceContent);
|
||||
const userMessage = buildPostUserMessage(sourceContent, context, previousPosts);
|
||||
|
||||
const messages: LLMMessage[] = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
|
||||
+69
-20
@@ -261,6 +261,40 @@ async function getNextUnprocessedContentItem(botId: string): Promise<ContentItem
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bot's previous posts for context.
|
||||
* Returns the content of the most recent posts to help avoid repetition.
|
||||
*
|
||||
* @param botId - The bot ID
|
||||
* @param limit - Maximum number of posts to fetch (default: 40)
|
||||
* @returns Array of post content strings
|
||||
*/
|
||||
async function getBotPreviousPosts(botId: string, limit: number = 40): Promise<string[]> {
|
||||
// Get bot to find its user ID
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get the bot's recent posts
|
||||
const recentPosts = await db.query.posts.findMany({
|
||||
where: eq(posts.userId, bot.userId),
|
||||
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
|
||||
limit,
|
||||
columns: {
|
||||
content: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Return just the content strings
|
||||
return recentPosts
|
||||
.map(p => p.content)
|
||||
.filter((content): content is string => !!content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a content item as processed.
|
||||
*
|
||||
@@ -507,6 +541,7 @@ export async function selectContentForPosting(
|
||||
* @param bot - The bot (from database)
|
||||
* @param contentItem - Optional content item to post about
|
||||
* @param context - Optional additional context
|
||||
* @param previousPosts - Optional array of previous post contents for context
|
||||
* @returns Generated post text
|
||||
*
|
||||
* Validates: Requirements 11.6
|
||||
@@ -514,7 +549,8 @@ export async function selectContentForPosting(
|
||||
export async function generatePostContent(
|
||||
bot: typeof bots.$inferSelect & { user: { handle: string } },
|
||||
contentItem?: ContentItem,
|
||||
context?: string
|
||||
context?: string,
|
||||
previousPosts?: string[]
|
||||
): Promise<string> {
|
||||
// Check if bot has API key
|
||||
try {
|
||||
@@ -539,7 +575,7 @@ export async function generatePostContent(
|
||||
|
||||
try {
|
||||
// Generate post (Requirement 11.6)
|
||||
const generatedContent = await generator.generatePost(contentItem, context);
|
||||
const generatedContent = await generator.generatePost(contentItem, context, previousPosts);
|
||||
|
||||
// Log the generation
|
||||
await logActivity(
|
||||
@@ -550,6 +586,7 @@ export async function generatePostContent(
|
||||
contentItemId: contentItem?.id,
|
||||
tokensUsed: generatedContent.tokensUsed,
|
||||
model: generatedContent.model,
|
||||
previousPostsCount: previousPosts?.length || 0,
|
||||
},
|
||||
true
|
||||
);
|
||||
@@ -995,15 +1032,21 @@ export async function triggerPost(
|
||||
});
|
||||
}
|
||||
|
||||
// Select content (Requirement 5.4)
|
||||
const contentItem = await selectContentForPosting(botId, sourceContentId);
|
||||
|
||||
if (!contentItem) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No content available for posting',
|
||||
errorCode: 'NO_CONTENT',
|
||||
};
|
||||
// Select content (Requirement 5.4) - optional, bots can post without sources
|
||||
let contentItem: ContentItem | null = null;
|
||||
|
||||
if (sourceContentId) {
|
||||
contentItem = await selectContentForPosting(botId, sourceContentId);
|
||||
if (!contentItem) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Specified content not found',
|
||||
errorCode: 'CONTENT_NOT_FOUND',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Try to get content, but don't fail if none available
|
||||
contentItem = await selectContentForPosting(botId);
|
||||
}
|
||||
|
||||
// Get bot from database for generation
|
||||
@@ -1020,8 +1063,12 @@ export async function triggerPost(
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch previous posts for context (helps avoid repetition)
|
||||
const previousPosts = await getBotPreviousPosts(botId, 40);
|
||||
|
||||
// Generate post content (Requirement 11.6)
|
||||
let postContent = await generatePostContent(dbBot, contentItem, context);
|
||||
// Content item is optional - bot can generate posts based on personality alone
|
||||
let postContent = await generatePostContent(dbBot, contentItem || undefined, context, previousPosts);
|
||||
|
||||
// Sanitize content
|
||||
postContent = sanitizePostContent(postContent);
|
||||
@@ -1036,7 +1083,7 @@ export async function triggerPost(
|
||||
'error',
|
||||
{
|
||||
type: 'validation',
|
||||
contentItemId: contentItem.id,
|
||||
contentItemId: contentItem?.id || null,
|
||||
errors: validation.errors,
|
||||
content: postContent,
|
||||
},
|
||||
@@ -1048,21 +1095,23 @@ export async function triggerPost(
|
||||
success: false,
|
||||
error: `Post validation failed: ${validation.errors.join(', ')}`,
|
||||
errorCode: 'VALIDATION_FAILED',
|
||||
contentItem,
|
||||
contentItem: contentItem || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create post in database with source URL for link preview
|
||||
const post = await createPostInDatabase(botId, postContent, contentItem.url);
|
||||
// Create post in database with source URL for link preview (if content item exists)
|
||||
const post = await createPostInDatabase(botId, postContent, contentItem?.url);
|
||||
|
||||
// Record post for rate limiting
|
||||
if (!skipRateLimitCheck) {
|
||||
await recordPost(botId);
|
||||
}
|
||||
|
||||
// Mark content item as processed
|
||||
await markContentItemProcessed(contentItem.id, post.id);
|
||||
// Mark content item as processed (only if we used one)
|
||||
if (contentItem) {
|
||||
await markContentItemProcessed(contentItem.id, post.id);
|
||||
}
|
||||
|
||||
// Log successful post creation
|
||||
await logActivity(
|
||||
@@ -1070,7 +1119,7 @@ export async function triggerPost(
|
||||
'post_created',
|
||||
{
|
||||
postId: post.id,
|
||||
contentItemId: contentItem.id,
|
||||
contentItemId: contentItem?.id || null,
|
||||
contentLength: postContent.length,
|
||||
},
|
||||
true
|
||||
@@ -1082,7 +1131,7 @@ export async function triggerPost(
|
||||
return {
|
||||
success: true,
|
||||
post,
|
||||
contentItem,
|
||||
contentItem: contentItem || undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
// Log error
|
||||
|
||||
Reference in New Issue
Block a user