mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-27 20:29:44 +02:00
Read prebuilt templates from code directly
This commit is contained in:
parent
726559de76
commit
2a1143c833
5 changed files with 133 additions and 102 deletions
|
|
@ -5,7 +5,7 @@ import { authCheck } from "./auth.actions";
|
||||||
import { MongoDBAssistantTemplatesRepository } from '@/src/infrastructure/repositories/mongodb.assistant-templates.repository';
|
import { MongoDBAssistantTemplatesRepository } from '@/src/infrastructure/repositories/mongodb.assistant-templates.repository';
|
||||||
import { prebuiltTemplates } from '@/app/lib/prebuilt-cards';
|
import { prebuiltTemplates } from '@/app/lib/prebuilt-cards';
|
||||||
import { USE_AUTH } from '@/app/lib/feature_flags';
|
import { USE_AUTH } from '@/app/lib/feature_flags';
|
||||||
import { ensureLibraryTemplatesSeeded } from '@/app/lib/assistant_templates_seed';
|
// import { ensureLibraryTemplatesSeeded } from '@/app/lib/assistant_templates_seed';
|
||||||
|
|
||||||
const repo = new MongoDBAssistantTemplatesRepository();
|
const repo = new MongoDBAssistantTemplatesRepository();
|
||||||
|
|
||||||
|
|
@ -38,101 +38,114 @@ const CreateTemplateSchema = z.object({
|
||||||
thumbnailUrl: z.string().url().optional(),
|
thumbnailUrl: z.string().url().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function listAssistantTemplates(request: z.infer<typeof ListTemplatesSchema>) {
|
type ListResponse = { items: any[]; nextCursor: string | null };
|
||||||
|
|
||||||
|
function buildPrebuiltList(params: z.infer<typeof ListTemplatesSchema>): ListResponse {
|
||||||
|
const allPrebuilt = Object.entries(prebuiltTemplates).map(([key, tpl]) => ({
|
||||||
|
id: `prebuilt:${key}`,
|
||||||
|
name: (tpl as any).name || key,
|
||||||
|
description: (tpl as any).description || '',
|
||||||
|
category: (tpl as any).category || 'Other',
|
||||||
|
tools: (tpl as any).tools || [],
|
||||||
|
createdAt: (tpl as any).lastUpdatedAt || undefined,
|
||||||
|
source: 'library' as const,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let filtered = allPrebuilt;
|
||||||
|
if (params.category) {
|
||||||
|
filtered = filtered.filter(t => t.category === params.category);
|
||||||
|
}
|
||||||
|
if (params.search) {
|
||||||
|
const q = params.search.toLowerCase();
|
||||||
|
filtered = filtered.filter(t =>
|
||||||
|
t.name.toLowerCase().includes(q) ||
|
||||||
|
t.description.toLowerCase().includes(q) ||
|
||||||
|
t.category.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIndex = params.cursor ? parseInt(params.cursor, 10) || 0 : 0;
|
||||||
|
const endIndex = Math.min(startIndex + params.limit, filtered.length);
|
||||||
|
const pageItems = filtered.slice(startIndex, endIndex);
|
||||||
|
const nextCursor = endIndex < filtered.length ? String(endIndex) : null;
|
||||||
|
|
||||||
|
return { items: pageItems, nextCursor };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAssistantTemplates(request: z.infer<typeof ListTemplatesSchema>): Promise<ListResponse> {
|
||||||
const user = await authCheck();
|
const user = await authCheck();
|
||||||
|
|
||||||
// Throttle best-effort library reconcile/seed to avoid repeated bursts
|
// Prebuilt templates should never be seeded to DB
|
||||||
try {
|
|
||||||
const last = (globalThis as any).__lastLibrarySeedAt as number | undefined;
|
|
||||||
const now = Date.now();
|
|
||||||
if (!last || now - last > 60_000) { // at most once per minute
|
|
||||||
(globalThis as any).__lastLibrarySeedAt = now;
|
|
||||||
void ensureLibraryTemplatesSeeded();
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
const params = ListTemplatesSchema.parse(request);
|
const params = ListTemplatesSchema.parse(request);
|
||||||
|
|
||||||
// If source specified, query that subset; otherwise return combined from the unified collection
|
// If source specified, return that subset; for 'library' use in-memory prebuilt from code
|
||||||
if (params.source === 'library' || params.source === 'community') {
|
if (params.source === 'library') {
|
||||||
|
const { items, nextCursor } = buildPrebuiltList(params);
|
||||||
|
return { items: serializeTemplates(items), nextCursor };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.source === 'community') {
|
||||||
const result = await repo.list({
|
const result = await repo.list({
|
||||||
category: params.category,
|
category: params.category,
|
||||||
search: params.search,
|
search: params.search,
|
||||||
featured: params.featured,
|
featured: params.featured,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
source: params.source,
|
source: 'community',
|
||||||
}, params.cursor, params.limit);
|
}, params.cursor, params.limit);
|
||||||
|
|
||||||
// Add isLiked status to each template
|
|
||||||
const itemsWithLikeStatus = await addLikeStatusToTemplates(result.items, user.id);
|
const itemsWithLikeStatus = await addLikeStatusToTemplates(result.items, user.id);
|
||||||
|
return { ...result, items: serializeTemplates(itemsWithLikeStatus) };
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
items: serializeTemplates(itemsWithLikeStatus)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No source: combine both subsets from the unified collection
|
// No source: return prebuilt from code + first page of community from DB
|
||||||
const [lib, com] = await Promise.all([
|
const prebuilt = buildPrebuiltList({ ...params, source: 'library' } as any).items;
|
||||||
repo.list({ category: params.category, search: params.search, featured: params.featured, isPublic: true, source: 'library' }, undefined, params.limit),
|
const communityPage = await repo.list({
|
||||||
repo.list({ category: params.category, search: params.search, featured: params.featured, isPublic: true, source: 'community' }, undefined, params.limit),
|
category: params.category,
|
||||||
]);
|
search: params.search,
|
||||||
|
featured: params.featured,
|
||||||
// Add isLiked status to all templates
|
isPublic: true,
|
||||||
const allTemplates = [...lib.items, ...com.items];
|
source: 'community',
|
||||||
const itemsWithLikeStatus = await addLikeStatusToTemplates(allTemplates, user.id);
|
}, undefined, params.limit);
|
||||||
|
const items = [...prebuilt, ...communityPage.items];
|
||||||
return {
|
return { items: serializeTemplates(items), nextCursor: null };
|
||||||
items: serializeTemplates(itemsWithLikeStatus),
|
|
||||||
nextCursor: null
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a specific template by ID with model transformation
|
// Get a specific template by ID with model transformation
|
||||||
export async function getAssistantTemplate(templateId: string) {
|
export async function getAssistantTemplate(templateId: string) {
|
||||||
const user = await authCheck();
|
const user = await authCheck();
|
||||||
|
|
||||||
const template = await repo.fetch(templateId);
|
// Prebuilt: load directly from code
|
||||||
if (!template) {
|
if (templateId.startsWith('prebuilt:')) {
|
||||||
throw new Error('Template not found');
|
const key = templateId.replace('prebuilt:', '');
|
||||||
}
|
const originalTemplate = prebuiltTemplates[key as keyof typeof prebuiltTemplates];
|
||||||
|
if (!originalTemplate) throw new Error('Template not found');
|
||||||
// Check if this is a prebuilt template by looking at tags
|
|
||||||
const isPrebuiltTemplate = template.tags.some(tag => tag.startsWith('prebuilt:') || tag === '__library__');
|
const defaultModel = process.env.PROVIDER_DEFAULT_MODEL || 'gpt-4.1';
|
||||||
|
const transformedWorkflow = JSON.parse(JSON.stringify(originalTemplate));
|
||||||
if (isPrebuiltTemplate) {
|
if (transformedWorkflow.agents && Array.isArray(transformedWorkflow.agents)) {
|
||||||
// For prebuilt templates, get the original JSON and apply fresh transformation
|
transformedWorkflow.agents.forEach((agent: any) => {
|
||||||
const prebuiltTag = template.tags.find(tag => tag.startsWith('prebuilt:'));
|
if (agent.model === '') {
|
||||||
if (prebuiltTag) {
|
agent.model = defaultModel;
|
||||||
const prebuiltKey = prebuiltTag.replace('prebuilt:', '');
|
|
||||||
const originalTemplate = prebuiltTemplates[prebuiltKey as keyof typeof prebuiltTemplates];
|
|
||||||
|
|
||||||
if (originalTemplate) {
|
|
||||||
// Apply model transformation to the original template
|
|
||||||
const defaultModel = process.env.PROVIDER_DEFAULT_MODEL || 'gpt-4.1';
|
|
||||||
const transformedWorkflow = JSON.parse(JSON.stringify(originalTemplate));
|
|
||||||
|
|
||||||
// Transform blank agent models to use the default model
|
|
||||||
if (transformedWorkflow.agents && Array.isArray(transformedWorkflow.agents)) {
|
|
||||||
transformedWorkflow.agents.forEach((agent: any) => {
|
|
||||||
if (agent.model === '') {
|
|
||||||
agent.model = defaultModel;
|
|
||||||
console.log(`[PrebuiltTemplate] Applied model ${defaultModel} to agent ${agent.name} in template ${prebuiltKey}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// Return the transformed template with database metadata but fresh workflow
|
|
||||||
const result = {
|
|
||||||
...template,
|
|
||||||
workflow: transformedWorkflow
|
|
||||||
};
|
|
||||||
|
|
||||||
return serializeTemplate(result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return minimal shape expected by callers
|
||||||
|
const result = {
|
||||||
|
id: templateId,
|
||||||
|
name: (originalTemplate as any).name || key,
|
||||||
|
description: (originalTemplate as any).description || '',
|
||||||
|
category: (originalTemplate as any).category || 'Other',
|
||||||
|
workflow: transformedWorkflow,
|
||||||
|
source: 'library' as const,
|
||||||
|
};
|
||||||
|
return serializeTemplate(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Community template from DB
|
||||||
|
const template = await repo.fetch(templateId);
|
||||||
|
if (!template) throw new Error('Template not found');
|
||||||
return serializeTemplate(template);
|
return serializeTemplate(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,15 @@ import interviewScheduler from './interview-scheduler.json';
|
||||||
import meetingPrepAssistant from './meeting-prep-assistant.json';
|
import meetingPrepAssistant from './meeting-prep-assistant.json';
|
||||||
import redditOnSlack from './reddit-on-slack.json';
|
import redditOnSlack from './reddit-on-slack.json';
|
||||||
import twitterSentiment from './twitter-sentiment.json';
|
import twitterSentiment from './twitter-sentiment.json';
|
||||||
import tweetAssistant from './tweet-assistant.json';
|
import tweetWithGeneratedImage from './tweet-with-generated-image.json';
|
||||||
import customerSupport from './customer-support.json';
|
import customerSupport from './customer-support.json';
|
||||||
import githubIssueToSlack from './github-issue-to-slack.json';
|
import githubIssueToSlack from './github-issue-to-slack.json';
|
||||||
import githubPrToSlack from './github-pr-to-slack.json';
|
import githubPrToSlack from './github-pr-to-slack.json';
|
||||||
import eisenhowerEmailOrganizer from './eisenhower-email-organizer.json';
|
import eisenhowerEmailOrganizer from './eisenhower-email-organizer.json';
|
||||||
|
import test3 from './test3.json';
|
||||||
|
import test4 from './test4.json';
|
||||||
|
import test5 from './test5.json';
|
||||||
|
import test6 from './test6.json';
|
||||||
|
|
||||||
// Keep keys consistent with prior file basenames to avoid breaking links.
|
// Keep keys consistent with prior file basenames to avoid breaking links.
|
||||||
export const prebuiltTemplates = {
|
export const prebuiltTemplates = {
|
||||||
|
|
@ -19,10 +23,14 @@ export const prebuiltTemplates = {
|
||||||
'Meeting Prep Assistant': meetingPrepAssistant,
|
'Meeting Prep Assistant': meetingPrepAssistant,
|
||||||
'Reddit on Slack': redditOnSlack,
|
'Reddit on Slack': redditOnSlack,
|
||||||
'Twitter Sentiment': twitterSentiment,
|
'Twitter Sentiment': twitterSentiment,
|
||||||
'Tweet Assistant': tweetAssistant,
|
'Tweet with generated image': tweetWithGeneratedImage,
|
||||||
'Customer Support': customerSupport,
|
'Customer Support': customerSupport,
|
||||||
'GitHub Issue to Slack': githubIssueToSlack,
|
'GitHub Issue to Slack': githubIssueToSlack,
|
||||||
'GitHub PR to Slack': githubPrToSlack,
|
'GitHub PR to Slack': githubPrToSlack,
|
||||||
'Eisenhower Email Organizer': eisenhowerEmailOrganizer,
|
'Eisenhower Email Organizer': eisenhowerEmailOrganizer,
|
||||||
|
'Test 3': test3,
|
||||||
|
'Test 4': test4,
|
||||||
|
'Test 5': test5,
|
||||||
|
'Test 6': test6,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@ export function BuildAssistantSection() {
|
||||||
|
|
||||||
// Handle template like (unified for library and community) - now uses proper authentication
|
// Handle template like (unified for library and community) - now uses proper authentication
|
||||||
const handleTemplateLike = async (template: any) => {
|
const handleTemplateLike = async (template: any) => {
|
||||||
|
if (template.type === 'prebuilt') return;
|
||||||
try {
|
try {
|
||||||
const data = await toggleTemplateLike(template.id);
|
const data = await toggleTemplateLike(template.id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ interface AssistantCardProps {
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
getUniqueTools?: (item: any) => Array<{ name: string; logo?: string }>;
|
getUniqueTools?: (item: any) => Array<{ name: string; logo?: string }>;
|
||||||
|
// UI flags
|
||||||
|
hideLikes?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AssistantCard({
|
export function AssistantCard({
|
||||||
|
|
@ -89,7 +91,8 @@ export function AssistantCard({
|
||||||
onClick,
|
onClick,
|
||||||
loading = false,
|
loading = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
getUniqueTools
|
getUniqueTools,
|
||||||
|
hideLikes = false
|
||||||
}: AssistantCardProps) {
|
}: AssistantCardProps) {
|
||||||
const displayTools = getUniqueTools ? getUniqueTools({ tools }) : tools;
|
const displayTools = getUniqueTools ? getUniqueTools({ tools }) : tools;
|
||||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = React.useState(false);
|
const [isDescriptionExpanded, setIsDescriptionExpanded] = React.useState(false);
|
||||||
|
|
@ -271,20 +274,22 @@ export function AssistantCard({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
{!hideLikes && (
|
||||||
onClick={(e) => {
|
<button
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.preventDefault();
|
||||||
onLike?.();
|
e.stopPropagation();
|
||||||
}}
|
onLike?.();
|
||||||
className={clsx(
|
}}
|
||||||
"flex items-center gap-1 hover:text-red-500 transition-colors",
|
className={clsx(
|
||||||
isLiked && "text-red-500"
|
"flex items-center gap-1 hover:text-red-500 transition-colors",
|
||||||
)}
|
isLiked && "text-red-500"
|
||||||
>
|
)}
|
||||||
<Heart size={14} className={isLiked ? "fill-current" : ""} />
|
>
|
||||||
<span>{likeCount || 0}</span>
|
<Heart size={14} className={isLiked ? "fill-current" : ""} />
|
||||||
</button>
|
<span>{likeCount || 0}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export function UnifiedTemplatesSection({
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedType, setSelectedType] = useState<'prebuilt' | 'community'>('prebuilt');
|
const [selectedType, setSelectedType] = useState<'prebuilt' | 'community'>('prebuilt');
|
||||||
const [selectedCategories, setSelectedCategories] = useState<Set<string>>(new Set());
|
const [selectedCategories, setSelectedCategories] = useState<Set<string>>(new Set());
|
||||||
const [sortBy, setSortBy] = useState<'popular' | 'newest' | 'alphabetical'>('popular');
|
const [sortBy, setSortBy] = useState<'popular' | 'newest' | 'alphabetical'>('alphabetical');
|
||||||
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
|
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
const [pendingDeleteItem, setPendingDeleteItem] = useState<TemplateItem | null>(null);
|
const [pendingDeleteItem, setPendingDeleteItem] = useState<TemplateItem | null>(null);
|
||||||
|
|
@ -152,14 +152,15 @@ export function UnifiedTemplatesSection({
|
||||||
case 'alphabetical':
|
case 'alphabetical':
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
case 'popular':
|
case 'popular':
|
||||||
default:
|
// Only meaningful for community templates
|
||||||
// Normal sorting by like count when no user interaction
|
if (selectedType === 'community') {
|
||||||
const aLikes = Number(a.likeCount) || 0;
|
const aLikes = Number(a.likeCount) || 0;
|
||||||
const bLikes = Number(b.likeCount) || 0;
|
const bLikes = Number(b.likeCount) || 0;
|
||||||
if (bLikes !== aLikes) return bLikes - aLikes;
|
if (bLikes !== aLikes) return bLikes - aLikes;
|
||||||
const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
||||||
const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
||||||
if (bTime !== aTime) return bTime - aTime;
|
if (bTime !== aTime) return bTime - aTime;
|
||||||
|
}
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -213,7 +214,7 @@ export function UnifiedTemplatesSection({
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
setSelectedType('prebuilt');
|
setSelectedType('prebuilt');
|
||||||
setSelectedCategories(new Set());
|
setSelectedCategories(new Set());
|
||||||
setSortBy('popular');
|
setSortBy('alphabetical');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if any filters are active
|
// Check if any filters are active
|
||||||
|
|
@ -326,14 +327,16 @@ export function UnifiedTemplatesSection({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sort Dropdown */}
|
{/* Sort Dropdown (Popularity only for Community) */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<select
|
<select
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => setSortBy(e.target.value as any)}
|
onChange={(e) => setSortBy(e.target.value as any)}
|
||||||
className="w-44 h-8 px-4 pr-10 border border-gray-300 dark:border-gray-700 rounded-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 appearance-none text-sm hover:bg-gray-50 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
|
className="w-44 h-8 px-4 pr-10 border border-gray-300 dark:border-gray-700 rounded-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 appearance-none text-sm hover:bg-gray-50 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||||
>
|
>
|
||||||
<option value="popular">Most Popular</option>
|
{selectedType === 'community' && (
|
||||||
|
<option value="popular">Most Popular</option>
|
||||||
|
)}
|
||||||
<option value="newest">Newest First</option>
|
<option value="newest">Newest First</option>
|
||||||
<option value="alphabetical">A-Z</option>
|
<option value="alphabetical">A-Z</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -428,6 +431,7 @@ export function UnifiedTemplatesSection({
|
||||||
} : undefined}
|
} : undefined}
|
||||||
isLiked={item.isLiked}
|
isLiked={item.isLiked}
|
||||||
templateType={item.type}
|
templateType={item.type}
|
||||||
|
hideLikes={item.type === 'prebuilt'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue