Skip to content

Commit da397c3

Browse files
MrCoderclaude
andcommitted
feat: add AI title generation with OpenAI integration
- Add AI title generation service for sequence diagrams - Integrate OpenAI GPT-4 API for intelligent title suggestions - Implement content analysis to infer diagram domain and context - Add rate limiting (10 requests/minute) and 24-hour caching - Include comprehensive fallback logic for offline/error scenarios - Add Generate Title button in main header for authenticated users - Track usage with Mixpanel analytics - Support domain inference across e-commerce, auth, banking, and more 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 34fefbf commit da397c3

File tree

7 files changed

+1772
-360
lines changed

7 files changed

+1772
-360
lines changed

functions/firestore-debug.log

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
July 15, 2025 10:39:51 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketServer start
2+
INFO: Started WebSocket server on ws://127.0.0.1:9150
3+
API endpoint: http://127.0.0.1:8080
4+
If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run:
5+
6+
export FIRESTORE_EMULATOR_HOST=127.0.0.1:8080
7+
8+
If you are running a Firestore in Datastore Mode project, run:
9+
10+
export DATASTORE_EMULATOR_HOST=127.0.0.1:8080
11+
12+
Note: Support for Datastore Mode is in preview. If you encounter any bugs please file at https://github.com/firebase/firebase-tools/issues.
13+
Dev App Server is now running.
14+
15+
*** shutting down gRPC server since JVM is shutting down
16+
*** server shut down

functions/index.js

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,354 @@ function isJSONString(str) {
179179
return false;
180180
}
181181
}
182+
183+
// AI Title Generation Function
184+
exports.generateTitle = functions.https.onRequest(async (req, res) => {
185+
// Enable CORS
186+
res.set('Access-Control-Allow-Origin', '*');
187+
res.set('Access-Control-Allow-Methods', 'POST, OPTIONS');
188+
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
189+
190+
if (req.method === 'OPTIONS') {
191+
res.status(200).send('');
192+
return;
193+
}
194+
195+
if (req.method !== 'POST') {
196+
res.status(405).send('Method Not Allowed');
197+
return;
198+
}
199+
200+
try {
201+
// Verify authentication
202+
const auth = req.get('Authorization');
203+
if (!auth) {
204+
res.status(401).send('Unauthorized');
205+
return;
206+
}
207+
208+
const decoded = await verifyIdToken(auth);
209+
const userId = decoded.uid;
210+
211+
// Get diagram content from request
212+
const { content } = req.body;
213+
if (!content) {
214+
res.status(400).send('Missing diagram content');
215+
return;
216+
}
217+
218+
// Generate title using AI
219+
const title = await generateTitleFromContent(content, userId);
220+
221+
// Track usage
222+
mixpanel.track('ai_title_generated', {
223+
distinct_id: userId,
224+
event_category: 'ai',
225+
displayProductName: 'FireWeb',
226+
});
227+
228+
res.status(200).json({ title });
229+
} catch (error) {
230+
console.error('AI title generation error:', error);
231+
res.status(500).json({ error: 'Failed to generate title' });
232+
}
233+
});
234+
235+
async function generateTitleFromContent(content, userId) {
236+
// Analyze ZenUML content
237+
const analysis = analyzeZenUMLContent(content);
238+
239+
// Check cache first
240+
const cacheKey = generateCacheKey(content);
241+
const cachedTitle = await getCachedTitle(cacheKey);
242+
if (cachedTitle) {
243+
return cachedTitle;
244+
}
245+
246+
// Check rate limiting
247+
const rateLimitKey = `rate_limit_${userId}`;
248+
const rateLimitAllowed = await checkRateLimit(rateLimitKey);
249+
if (!rateLimitAllowed) {
250+
throw new Error('Rate limit exceeded');
251+
}
252+
253+
try {
254+
// Call OpenAI API
255+
const openaiApiKey = functions.config().openai?.api_key;
256+
if (!openaiApiKey) {
257+
throw new Error('OpenAI API key not configured');
258+
}
259+
260+
const title = await callOpenAI(analysis, content, openaiApiKey);
261+
262+
// Cache the result
263+
await setCachedTitle(cacheKey, title);
264+
265+
return title;
266+
} catch (error) {
267+
console.error('OpenAI API error:', error);
268+
// Return fallback title
269+
return generateFallbackTitle(analysis);
270+
}
271+
}
272+
273+
function analyzeZenUMLContent(code) {
274+
if (!code || typeof code !== 'string') {
275+
return { participants: [], methods: [], keywords: [], domain: '' };
276+
}
277+
278+
const lines = code.split('\n').map(line => line.trim());
279+
const participants = new Set();
280+
const methods = new Set();
281+
const keywords = new Set();
282+
const comments = [];
283+
284+
lines.forEach(line => {
285+
if (line.startsWith('//')) {
286+
comments.push(line.substring(2).trim());
287+
return;
288+
}
289+
290+
if (!line || line.startsWith('/*') || line.startsWith('*')) return;
291+
292+
const methodCall = line.match(/(\w+)\.(\w+)\s*\(/);
293+
if (methodCall) {
294+
participants.add(methodCall[1]);
295+
methods.add(methodCall[2]);
296+
}
297+
298+
const participant = line.match(/^(\w+)\s*$/);
299+
if (participant) {
300+
participants.add(participant[1]);
301+
}
302+
303+
const businessTerms = line.match(/\b(get|create|update|delete|process|handle|manage|validate|authenticate|authorize|login|logout|register|book|user|library|order|payment|account|customer|product|service|api|database|auth|admin|dashboard|report|search|filter|export|import|notification|email|message|chat|upload|download|sync|backup|restore|config|setting|profile|cart|checkout|invoice|receipt|transaction|transfer|withdraw|deposit|balance|history|analytics|metric|log|error|warning|info|debug|trace|monitor|alert|health|status|version|release|deploy|build|test|dev|prod|staging|local|remote|cloud|server|client|web|mobile|desktop|app|system|platform|framework|library|tool|util|helper|service|component|module|plugin|extension|widget|control|element|item|entity|model|view|controller|route|middleware|filter|guard|interceptor|decorator|factory|builder|manager|handler|processor|validator|parser|formatter|serializer|deserializer|encoder|decoder|compressor|decompressor|optimizer|analyzer|generator|converter|transformer|mapper|adapter|wrapper|proxy|cache|store|repository|dao|dto|vo|bo|po|entity|aggregate|event|command|query|request|response|result|error|exception|success|failure|pending|loading|complete|cancel|timeout|retry|fallback|default|custom|standard|advanced|basic|simple|complex|manual|automatic|sync|async|batch|single|multi|global|local|public|private|internal|external|static|dynamic|virtual|abstract|concrete|interface|implementation|specification|definition|declaration|configuration|initialization|finalization|cleanup|setup|teardown|start|stop|pause|resume|reset|refresh|reload|update|upgrade|downgrade|migrate|rollback|commit|rollback|save|load|read|write|execute|run|invoke|call|send|receive|publish|subscribe|listen|watch|observe|trigger|emit|dispatch|broadcast|notify|alert|warn|info|debug|trace|log|audit|track|monitor|measure|count|sum|average|min|max|sort|filter|search|find|select|insert|update|delete|create|destroy|build|compile|parse|render|format|validate|verify|check|test|assert|expect|mock|stub|spy|fake|dummy|placeholder|template|example|sample|demo|prototype|proof|concept|idea|plan|design|architecture|pattern|best|practice|convention|standard|guideline|rule|policy|procedure|workflow|process|step|phase|stage|cycle|iteration|loop|condition|branch|merge|split|join|fork|clone|copy|move|rename|replace|swap|exchange|convert|transform|map|reduce|filter|fold|zip|unzip|pack|unpack|serialize|deserialize|encode|decode|encrypt|decrypt|hash|salt|token|key|secret|password|username|email|phone|address|name|title|description|comment|note|tag|label|category|type|kind|class|group|set|list|array|map|dict|hash|tree|graph|node|edge|link|path|route|url|uri|endpoint|resource|entity|object|value|property|attribute|field|column|row|record|document|file|folder|directory|project|workspace|environment|context|scope|namespace|package|module|library|framework|tool|utility|service|component|widget|control|element|item|entity|model|view|controller|presenter|viewmodel|adapter|wrapper|proxy|facade|decorator|observer|listener|handler|callback|promise|future|task|job|worker|thread|process|queue|stack|heap|memory|storage|database|cache|session|cookie|token|authorization|authentication|permission|role|user|admin|guest|anonymous|public|private|protected|internal|external|readonly|writeonly|readwrite|immutable|mutable|const|var|let|final|static|abstract|virtual|override|implement|extend|inherit|compose|mixin|trait|interface|class|struct|enum|union|tuple|record|generic|template|macro|annotation|attribute|metadata|reflection|introspection|serialization|deserialization|marshalling|unmarshalling|encoding|decoding|compression|decompression|optimization|performance|scalability|reliability|availability|consistency|durability|security|privacy|compliance|governance|monitoring|logging|debugging|profiling|testing|validation|verification|documentation|specification|requirement|design|implementation|deployment|maintenance|support|troubleshooting|debugging|optimization|refactoring|migration|upgrade|deprecation|retirement|sunsetting)\b/gi);
304+
if (businessTerms) {
305+
businessTerms.forEach(term => keywords.add(term.toLowerCase()));
306+
}
307+
});
308+
309+
const domainAnalysis = inferDomain(Array.from(participants), Array.from(methods), Array.from(keywords));
310+
311+
return {
312+
participants: Array.from(participants),
313+
methods: Array.from(methods),
314+
keywords: Array.from(keywords),
315+
comments,
316+
domain: domainAnalysis
317+
};
318+
}
319+
320+
function inferDomain(participants, methods, keywords) {
321+
const domainPatterns = {
322+
'E-commerce': ['order', 'cart', 'checkout', 'payment', 'product', 'customer', 'inventory', 'shipping'],
323+
'Authentication': ['login', 'logout', 'register', 'auth', 'user', 'password', 'token', 'session'],
324+
'Library Management': ['book', 'library', 'borrow', 'return', 'catalog', 'member', 'loan'],
325+
'Banking': ['account', 'transaction', 'transfer', 'balance', 'deposit', 'withdraw', 'payment'],
326+
'Content Management': ['content', 'article', 'post', 'publish', 'edit', 'draft', 'media'],
327+
'Communication': ['message', 'chat', 'email', 'notification', 'send', 'receive', 'broadcast'],
328+
'Data Processing': ['process', 'analyze', 'transform', 'import', 'export', 'sync', 'backup'],
329+
'API Integration': ['api', 'endpoint', 'request', 'response', 'service', 'client', 'server'],
330+
'User Management': ['user', 'profile', 'admin', 'role', 'permission', 'setting', 'preference'],
331+
'File Management': ['file', 'upload', 'download', 'storage', 'folder', 'document', 'media']
332+
};
333+
334+
const allTerms = [...participants, ...methods, ...keywords].map(term => term.toLowerCase());
335+
336+
let bestMatch = { domain: 'General', score: 0 };
337+
338+
for (const [domain, patterns] of Object.entries(domainPatterns)) {
339+
const matches = patterns.filter(pattern => allTerms.some(term => term.includes(pattern)));
340+
const score = matches.length;
341+
342+
if (score > bestMatch.score) {
343+
bestMatch = { domain, score };
344+
}
345+
}
346+
347+
return bestMatch.domain;
348+
}
349+
350+
function generateCacheKey(content) {
351+
let hash = 0;
352+
for (let i = 0; i < content.length; i++) {
353+
const char = content.charCodeAt(i);
354+
hash = ((hash << 5) - hash) + char;
355+
hash = hash & hash;
356+
}
357+
return hash.toString(36);
358+
}
359+
360+
async function getCachedTitle(cacheKey) {
361+
try {
362+
const doc = await db.collection('title_cache').doc(cacheKey).get();
363+
if (doc.exists) {
364+
const data = doc.data();
365+
// Check if cache is still valid (24 hours)
366+
const now = Date.now();
367+
if (now - data.timestamp < 24 * 60 * 60 * 1000) {
368+
return data.title;
369+
}
370+
}
371+
} catch (error) {
372+
console.error('Cache read error:', error);
373+
}
374+
return null;
375+
}
376+
377+
async function setCachedTitle(cacheKey, title) {
378+
try {
379+
await db.collection('title_cache').doc(cacheKey).set({
380+
title,
381+
timestamp: Date.now()
382+
});
383+
} catch (error) {
384+
console.error('Cache write error:', error);
385+
}
386+
}
387+
388+
async function checkRateLimit(rateLimitKey) {
389+
try {
390+
const doc = await db.collection('rate_limits').doc(rateLimitKey).get();
391+
const now = Date.now();
392+
393+
if (doc.exists) {
394+
const data = doc.data();
395+
const windowStart = now - (60 * 1000); // 1 minute window
396+
397+
// Filter requests within the current window
398+
const recentRequests = (data.requests || []).filter(timestamp => timestamp > windowStart);
399+
400+
if (recentRequests.length >= 10) { // Max 10 requests per minute
401+
return false;
402+
}
403+
404+
// Add current request
405+
recentRequests.push(now);
406+
407+
await db.collection('rate_limits').doc(rateLimitKey).set({
408+
requests: recentRequests
409+
});
410+
} else {
411+
// First request
412+
await db.collection('rate_limits').doc(rateLimitKey).set({
413+
requests: [now]
414+
});
415+
}
416+
417+
return true;
418+
} catch (error) {
419+
console.error('Rate limit check error:', error);
420+
return false;
421+
}
422+
}
423+
424+
async function callOpenAI(analysis, originalContent, apiKey) {
425+
const prompt = buildPrompt(analysis, originalContent);
426+
427+
const requestOptions = {
428+
hostname: 'api.openai.com',
429+
port: 443,
430+
path: '/v1/chat/completions',
431+
method: 'POST',
432+
headers: {
433+
'Content-Type': 'application/json',
434+
'Authorization': `Bearer ${apiKey}`
435+
}
436+
};
437+
438+
const postData = JSON.stringify({
439+
model: 'gpt-4',
440+
messages: [
441+
{
442+
role: 'system',
443+
content: 'You are a helpful assistant that generates concise, descriptive titles for sequence diagrams. Keep titles under 50 characters and focus on the main business process or interaction.'
444+
},
445+
{
446+
role: 'user',
447+
content: prompt
448+
}
449+
],
450+
max_tokens: 50,
451+
temperature: 0.7
452+
});
453+
454+
return new Promise((resolve, reject) => {
455+
const req = https.request(requestOptions, (res) => {
456+
let data = '';
457+
458+
res.on('data', (chunk) => {
459+
data += chunk;
460+
});
461+
462+
res.on('end', () => {
463+
try {
464+
const response = JSON.parse(data);
465+
466+
if (res.statusCode !== 200) {
467+
reject(new Error(`OpenAI API error: ${response.error?.message || 'Unknown error'}`));
468+
return;
469+
}
470+
471+
const title = response.choices[0]?.message?.content?.trim();
472+
473+
if (!title) {
474+
reject(new Error('Empty response from OpenAI'));
475+
return;
476+
}
477+
478+
resolve(title.replace(/['"]/g, '').substring(0, 50));
479+
} catch (parseError) {
480+
reject(new Error(`Failed to parse OpenAI response: ${parseError.message}`));
481+
}
482+
});
483+
});
484+
485+
req.on('error', (error) => {
486+
reject(new Error(`OpenAI request failed: ${error.message}`));
487+
});
488+
489+
req.write(postData);
490+
req.end();
491+
});
492+
}
493+
494+
function buildPrompt(analysis, originalContent) {
495+
const { participants, methods, keywords, comments, domain } = analysis;
496+
497+
return `Generate a concise title for this sequence diagram:
498+
499+
Domain: ${domain}
500+
Participants: ${participants.join(', ')}
501+
Methods: ${methods.join(', ')}
502+
Key Terms: ${keywords.slice(0, 10).join(', ')}
503+
${comments.length > 0 ? `Comments: ${comments.join(' ')}` : ''}
504+
505+
Original content preview:
506+
${originalContent.substring(0, 200)}...
507+
508+
Generate a title that captures the main business process or interaction. Keep it under 50 characters.`;
509+
}
510+
511+
function generateFallbackTitle(analysis) {
512+
const { participants, methods, domain } = analysis;
513+
514+
if (domain && domain !== 'General') {
515+
return `${domain} Flow`;
516+
}
517+
518+
if (participants.length > 0 && methods.length > 0) {
519+
const primaryParticipant = participants[0];
520+
const primaryMethod = methods.find(m =>
521+
['get', 'create', 'update', 'delete', 'process', 'handle', 'manage'].includes(m.toLowerCase())
522+
) || methods[0];
523+
524+
return `${primaryParticipant} ${primaryMethod}`;
525+
}
526+
527+
if (participants.length > 0) {
528+
return `${participants[0]} Interaction`;
529+
}
530+
531+
return 'Sequence Diagram';
532+
}

0 commit comments

Comments
 (0)