Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion kits/peer-demo-showcase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ https://agent-kit-nu.vercel.app/

---


## 💡 Why This Kit Matters for Aman Sharma & the Lamatic.ai Team

As a co-founder of **Lamatic.ai**, **Aman Sharma** and the engineering team are building the future of agentic AI workflow orchestration. This project serves as a showcase demonstration and template for **Lamatic AgentKit**:
Expand Down
1 change: 1 addition & 0 deletions kits/peer-demo-showcase/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This agent accepts a GitHub repository URL, crawls the README, extracts project
- github_url: The GitHub repository URL of the submitted project
- builder_name: Name of the person submitting
- contact_email: Contact email of the submitter
- sponsors_list: Comma-separated string of active sponsor track names available for matching (e.g., "Google Cloud, Vercel, Supabase")

## Outputs
- project_title: Extracted project name
Expand Down
4 changes: 1 addition & 3 deletions kits/peer-demo-showcase/apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@

## 📹 Video Walkthrough (Loom)

[![Watch Demo](https://img.shields.io/badge/Loom_Demo-Watch_Video-625DF5?style=for-the-badge&logo=loom&logoColor=white)](YOUR_LOOM_VIDEO_LINK_HERE)

> 📌 **Insert Loom Link**: Replace `YOUR_LOOM_VIDEO_LINK_HERE` with your recorded 2-minute video walkthrough link!
[![Watch Demo](https://img.shields.io/badge/Loom_Demo-Watch_Video-625DF5?style=for-the-badge&logo=loom&logoColor=white)](https://www.loom.com/share/ee030ec4015d4bc299da327ec0dd34e5)

---

Expand Down
44 changes: 30 additions & 14 deletions kits/peer-demo-showcase/apps/actions/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { createAdminSession, revokeAdminSession, createJudgeSession, revokeJudgeSession } from '../lib/session';
import { verifyJudgeCredentials } from './orchestrate';

const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;

Expand All @@ -12,9 +14,8 @@ export async function login(password: string) {

if (password === ADMIN_PASSWORD) {
const cookieStore = await cookies();
// Simple obscuration so the raw password isn't visible in the DevTools Cookies tab
const obscuredToken = btoa(password).split('').reverse().join('');
cookieStore.set('admin_session', obscuredToken, {
const sessionToken = createAdminSession(60 * 60 * 2 * 1000);
cookieStore.set('admin_session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
Expand All @@ -29,37 +30,52 @@ export async function login(password: string) {

export async function logout() {
const cookieStore = await cookies();
const sessionCookie = cookieStore.get('admin_session');
if (sessionCookie?.value) {
revokeAdminSession(sessionCookie.value);
}
cookieStore.delete('admin_session');
redirect('/admin/login');
}

export async function judgeLogin(password: string, name?: string) {
if (!password) {
return { success: false, error: 'Password is required' };
}

const { valid, judgeName } = await verifyJudgeCredentials(password, name);
if (!valid) {
return { success: false, error: 'Invalid judge credentials' };
}

const cookieStore = await cookies();
const obscuredToken = btoa(password).split('').reverse().join('');
const sessionToken = createJudgeSession(60 * 60 * 2 * 1000);

cookieStore.set('judge_session', obscuredToken, {
cookieStore.set('judge_session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 2 // 2 hours auto-expire
});

if (name) {
cookieStore.set('judge_name', name, {
httpOnly: false,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 2 // 2 hours auto-expire
});
}
cookieStore.set('judge_name', judgeName, {
httpOnly: false,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 2 // 2 hours auto-expire
});

return { success: true };
}

export async function judgeLogout() {
const cookieStore = await cookies();
const sessionCookie = cookieStore.get('judge_session');
if (sessionCookie?.value) {
revokeJudgeSession(sessionCookie.value);
}
cookieStore.delete('judge_session');
cookieStore.delete('judge_name');
redirect('/judge/login');
Expand Down
102 changes: 85 additions & 17 deletions kits/peer-demo-showcase/apps/actions/orchestrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ async function sendConfirmationEmail({
if (res.error) {
console.error('Resend email error:', res.error);
if (res.error.message?.includes('only send testing emails to your own email address')) {
console.warn(`[Resend Notice] Resend testing domain onboarding@resend.dev requires 'to' address to be the account owner email (avadhutscasual@gmail.com). Verify a custom domain at resend.com/domains to send to any recipient.`);
const accountOwner = process.env.RESEND_ACCOUNT_EMAIL ? ` (${process.env.RESEND_ACCOUNT_EMAIL})` : '';
console.warn(`[Resend Notice] Resend testing domain onboarding@resend.dev requires 'to' address to be the registered account owner email${accountOwner}. Verify a custom domain at resend.com/domains to send to any recipient.`);
}
} else {
console.log(`Successfully dispatched Resend confirmation email to ${to}`);
Expand Down Expand Up @@ -215,6 +216,23 @@ export async function submitProject(
return result;
}

/**
* ARCHITECTURAL NOTICE: DEMO MOCK FALLBACK STORES
*
* The MOCK_* mutable objects in this server module (MOCK_UPVOTES, MOCK_STATUSES,
* MOCK_SPONSORS, MOCK_SCORES, MOCK_JUDGES, MOCK_EVENT_CONFIG) serve as fallback
* in-memory data stores when Lamatic.ai flow environment variables are not present.
*
* Ephemeral & Instance-Local Nature:
* - These stores are instance-local and held in Node.js module memory on the current server process.
* - Fallback-mode writes (upvotes, statuses, sponsors, judge accounts, scores, event config)
* will reset upon redeployments, container restarts, or serverless cold starts.
* - Concurrent serverless instances maintain isolated memory states and may diverge.
*
* Production Deployment:
* - Configure all active LAMATIC_*_FLOW_ID environment variables in .env.local to route all
* queries and mutations directly to persistent Lamatic Cloud D1 database tables.
*/
const MOCK_UPVOTES: Record<string, number> = {};

export async function getSubmissions() {
Expand Down Expand Up @@ -333,6 +351,21 @@ export async function updateProjectStatus(id: string, status: string) {
return { status: 'success' };
}

export async function updateProjectSponsor(id: string, matched_sponsor: string) {
if (!id) throw new Error('Submission ID is required');

const flowId = process.env.LAMATIC_SUBMISSIONS_MANAGER_FLOW_ID || process.env.LAMATIC_UPDATE_STATUS_FLOW_ID;
if (flowId) {
try {
await lamaticClient.executeFlow(flowId, { action: 'update_sponsor', id, matched_sponsor });
} catch (err: any) {
console.warn('Lamatic updateProjectSponsor executeFlow failed:', err.message);
}
}

return { status: 'success' };
}

export async function resubmitProject(
id: string,
githubUrl: string,
Expand Down Expand Up @@ -476,11 +509,35 @@ let MOCK_SCORES: JudgeScore[] = [
}
];

let MOCK_JUDGES = [
{ id: 'j1', name: 'Judge Sarah', email: 'sarah@judge.com', password: 'judge' },
{ id: 'j2', name: 'Judge Alex', email: 'alex@judge.com', password: 'judge' }
let MOCK_JUDGES: Array<{ id: string; name: string; email: string; password?: string }> = [
{ id: 'j1', name: 'Judge Sarah', email: 'sarah@judge.com' },
{ id: 'j2', name: 'Judge Alex', email: 'alex@judge.com' }
];

export async function verifyJudgeCredentials(password: string, name?: string): Promise<{ valid: boolean; judgeName: string }> {
const configuredPassword = process.env.JUDGE_PASSWORD || process.env.ADMIN_PASSWORD || 'judge';
const judges = await manageJudges('list');
const list = Array.isArray(judges) ? judges : [];

const matchedJudge = list.find((j: any) => {
if (name && name.trim()) {
return j.name && j.name.toLowerCase().trim() === name.toLowerCase().trim();
}
return true;
});

const isPasswordValid = password === configuredPassword || list.some((j: any) => j.password && j.password === password);

if (isPasswordValid && (matchedJudge || !name || !name.trim())) {
return {
valid: true,
judgeName: (name && name.trim()) || matchedJudge?.name || 'Judge'
};
}

return { valid: false, judgeName: '' };
}

export async function submitScore(
projectId: string,
judgeName: string,
Expand Down Expand Up @@ -512,18 +569,23 @@ export async function submitScore(
return { status: 'success', score: newScore };
}

const response = await lamaticClient.executeFlow(flowId, {
action: 'submit_score',
project_id: projectId,
judge_name: judgeName,
innovation,
execution,
impact,
presentation,
notes
});
let response: any;
try {
response = await lamaticClient.executeFlow(flowId, {
action: 'submit_score',
project_id: projectId,
judge_name: judgeName,
innovation,
execution,
impact,
presentation,
notes
});
} catch (err: any) {
console.warn('Lamatic submitScore executeFlow failed:', err.message);
}

if (response.status === 'error') {
if (response && response.status === 'error') {
throw new Error(response.message || 'Failed to submit score');
}

Expand Down Expand Up @@ -647,8 +709,14 @@ export async function setEventConfig(key: string, value: string) {
return { status: 'success' };
}

const response = await lamaticClient.executeFlow(flowId, { action: 'set_config', key, value });
if (response.status === 'error') {
let response: any;
try {
response = await lamaticClient.executeFlow(flowId, { action: 'set_config', key, value });
} catch (err: any) {
console.warn('Lamatic setEventConfig executeFlow failed:', err.message);
}

if (response && response.status === 'error') {
throw new Error(response.message || 'Failed to update config');
}

Expand Down
89 changes: 70 additions & 19 deletions kits/peer-demo-showcase/apps/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { ArrowLeft, Shield, Loader2, Plus, LogOut, Trash2, Trophy, Clock, CheckCircle2, Award, Calendar, UserPlus, UserCheck, Star } from 'lucide-react';
import { getSubmissions, getSponsors, addSponsor, deleteSubmission, updateProjectStatus, getEventConfig, setEventConfig, manageJudges, getScores, JudgeScore } from '../../actions/orchestrate';
import { getSubmissions, getSponsors, addSponsor, deleteSubmission, updateProjectStatus, updateProjectSponsor, getEventConfig, setEventConfig, manageJudges, getScores, JudgeScore } from '../../actions/orchestrate';
import { logout } from '../../actions/auth';
import { toast } from 'sonner';
import Dropdown from '../../components/Dropdown';
Expand All @@ -18,6 +18,25 @@ interface Submission {
status?: string;
}

const MANAGED_SPONSORS: Array<{ name: string; domain: string; logo: string }> = [
{ name: 'Google Cloud', domain: 'cloud.google.com', logo: 'https://logo.clearbit.com/cloud.google.com' },
{ name: 'Vercel', domain: 'vercel.com', logo: 'https://logo.clearbit.com/vercel.com' },
{ name: 'Supabase', domain: 'supabase.com', logo: 'https://logo.clearbit.com/supabase.com' },
{ name: 'Neon', domain: 'neon.tech', logo: 'https://logo.clearbit.com/neon.tech' },
{ name: 'Stitch', domain: 'stitch.com', logo: 'https://logo.clearbit.com/stitch.com' },
{ name: 'MongoDB', domain: 'mongodb.com', logo: 'https://logo.clearbit.com/mongodb.com' },
{ name: 'Resend', domain: 'resend.com', logo: 'https://logo.clearbit.com/resend.com' },
{ name: 'Lamatic.ai', domain: 'lamatic.ai', logo: 'https://logo.clearbit.com/lamatic.ai' },
{ name: 'OpenAI', domain: 'openai.com', logo: 'https://logo.clearbit.com/openai.com' },
{ name: 'GitHub', domain: 'github.com', logo: 'https://logo.clearbit.com/github.com' },
{ name: 'Anthropic', domain: 'anthropic.com', logo: 'https://logo.clearbit.com/anthropic.com' },
{ name: 'Stripe', domain: 'stripe.com', logo: 'https://logo.clearbit.com/stripe.com' },
{ name: 'Cloudflare', domain: 'cloudflare.com', logo: 'https://logo.clearbit.com/cloudflare.com' },
{ name: 'PostHog', domain: 'posthog.com', logo: 'https://logo.clearbit.com/posthog.com' },
{ name: 'Pinecone', domain: 'pinecone.io', logo: 'https://logo.clearbit.com/pinecone.io' },
{ name: 'LangChain', domain: 'langchain.com', logo: 'https://logo.clearbit.com/langchain.com' }
];

export default function AdminPage() {
const [submissions, setSubmissions] = useState<Submission[]>([]);
const [sponsors, setSponsors] = useState<string[]>([]);
Expand Down Expand Up @@ -100,9 +119,18 @@ export default function AdminPage() {

const handleSaveDeadline = async (e: React.FormEvent) => {
e.preventDefault();
if (!deadline || !deadline.trim()) {
toast.error('Please select or enter a valid deadline date.');
return;
}
const dateObj = new Date(deadline);
if (isNaN(dateObj.getTime())) {
toast.error('Invalid deadline date format. Please select a valid date.');
return;
}
try {
setSavingDeadline(true);
const isoDate = new Date(deadline).toISOString();
const isoDate = dateObj.toISOString();
await setEventConfig('submission_deadline', isoDate);
toast.success('Submission deadline updated successfully.');
} catch (err: any) {
Expand All @@ -114,9 +142,18 @@ export default function AdminPage() {

const handleSaveWinnerTimer = async (e: React.FormEvent) => {
e.preventDefault();
if (!winnerDeclarationTime || !winnerDeclarationTime.trim()) {
toast.error('Please select or enter a valid winner declaration date.');
return;
}
const dateObj = new Date(winnerDeclarationTime);
if (isNaN(dateObj.getTime())) {
toast.error('Invalid winner declaration date format. Please select a valid date.');
return;
}
try {
setSavingWinnerTimer(true);
const isoDate = new Date(winnerDeclarationTime).toISOString();
const isoDate = dateObj.toISOString();
await setEventConfig('winner_declaration_time', isoDate);
toast.success('Winner declaration countdown timer updated successfully!');
} catch (err: any) {
Expand Down Expand Up @@ -154,16 +191,20 @@ export default function AdminPage() {
}
};

const handleSponsorChange = (id: string, newSponsor: string) => {
setSubmissions((prev) =>
prev.map((sub) =>
sub.id === id ? { ...sub, matched_sponsor: newSponsor } : sub
)
);

const sub = submissions.find(s => s.id === id);
if (sub) {
toast.success(`Reassigned "${sub.project_title}" to ${newSponsor}`);
const handleSponsorChange = async (id: string, newSponsor: string) => {
const sub = submissions.find((s) => s.id === id);
try {
await updateProjectSponsor(id, newSponsor);
setSubmissions((prev) =>
prev.map((item) =>
item.id === id ? { ...item, matched_sponsor: newSponsor } : item
)
);
if (sub) {
toast.success(`Reassigned "${sub.project_title}" to ${newSponsor}`);
}
} catch (err: any) {
toast.error(err.message || 'Failed to reassign sponsor.');
}
};

Expand Down Expand Up @@ -271,17 +312,27 @@ export default function AdminPage() {
onChange={async (e) => {
const val = e.target.value;
setNewSponsorName(val);
if (val.length > 1) {
if (val.trim().length > 1) {
setFetchingSuggestions(true);
try {
const res = await fetch(`https://autocomplete.clearbit.com/v1/companies/suggest?query=${encodeURIComponent(val)}`);
if (res.ok) {
const data = await res.json();
setSuggestions(data);
const queryLower = val.toLowerCase().trim();
const matches = MANAGED_SPONSORS.filter(
(s) => s.name.toLowerCase().includes(queryLower) || s.domain.toLowerCase().includes(queryLower)
);

if (matches.length > 0) {
setSuggestions(matches);
setShowSuggestions(true);
} else {
setSuggestions([]);
setShowSuggestions(false);
toast.info('No matching managed sponsor found. You can enter custom details below.', { id: 'sponsor-lookup-notice' });
}
} catch (err) {
console.error('Failed to fetch suggestions', err);
console.error('Failed to fetch sponsor suggestions', err);
setSuggestions([]);
setShowSuggestions(false);
toast.error('Sponsorship lookup unavailable. You can enter the sponsor name manually.');
} finally {
setFetchingSuggestions(false);
}
Expand Down
Loading