@@ -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 ( g e t | c r e a t e | u p d a t e | d e l e t e | p r o c e s s | h a n d l e | m a n a g e | v a l i d a t e | a u t h e n t i c a t e | a u t h o r i z e | l o g i n | l o g o u t | r e g i s t e r | b o o k | u s e r | l i b r a r y | o r d e r | p a y m e n t | a c c o u n t | c u s t o m e r | p r o d u c t | s e r v i c e | a p i | d a t a b a s e | a u t h | a d m i n | d a s h b o a r d | r e p o r t | s e a r c h | f i l t e r | e x p o r t | i m p o r t | n o t i f i c a t i o n | e m a i l | m e s s a g e | c h a t | u p l o a d | d o w n l o a d | s y n c | b a c k u p | r e s t o r e | c o n f i g | s e t t i n g | p r o f i l e | c a r t | c h e c k o u t | i n v o i c e | r e c e i p t | t r a n s a c t i o n | t r a n s f e r | w i t h d r a w | d e p o s i t | b a l a n c e | h i s t o r y | a n a l y t i c s | m e t r i c | l o g | e r r o r | w a r n i n g | i n f o | d e b u g | t r a c e | m o n i t o r | a l e r t | h e a l t h | s t a t u s | v e r s i o n | r e l e a s e | d e p l o y | b u i l d | t e s t | d e v | p r o d | s t a g i n g | l o c a l | r e m o t e | c l o u d | s e r v e r | c l i e n t | w e b | m o b i l e | d e s k t o p | a p p | s y s t e m | p l a t f o r m | f r a m e w o r k | l i b r a r y | t o o l | u t i l | h e l p e r | s e r v i c e | c o m p o n e n t | m o d u l e | p l u g i n | e x t e n s i o n | w i d g e t | c o n t r o l | e l e m e n t | i t e m | e n t i t y | m o d e l | v i e w | c o n t r o l l e r | r o u t e | m i d d l e w a r e | f i l t e r | g u a r d | i n t e r c e p t o r | d e c o r a t o r | f a c t o r y | b u i l d e r | m a n a g e r | h a n d l e r | p r o c e s s o r | v a l i d a t o r | p a r s e r | f o r m a t t e r | s e r i a l i z e r | d e s e r i a l i z e r | e n c o d e r | d e c o d e r | c o m p r e s s o r | d e c o m p r e s s o r | o p t i m i z e r | a n a l y z e r | g e n e r a t o r | c o n v e r t e r | t r a n s f o r m e r | m a p p e r | a d a p t e r | w r a p p e r | p r o x y | c a c h e | s t o r e | r e p o s i t o r y | d a o | d t o | v o | b o | p o | e n t i t y | a g g r e g a t e | e v e n t | c o m m a n d | q u e r y | r e q u e s t | r e s p o n s e | r e s u l t | e r r o r | e x c e p t i o n | s u c c e s s | f a i l u r e | p e n d i n g | l o a d i n g | c o m p l e t e | c a n c e l | t i m e o u t | r e t r y | f a l l b a c k | d e f a u l t | c u s t o m | s t a n d a r d | a d v a n c e d | b a s i c | s i m p l e | c o m p l e x | m a n u a l | a u t o m a t i c | s y n c | a s y n c | b a t c h | s i n g l e | m u l t i | g l o b a l | l o c a l | p u b l i c | p r i v a t e | i n t e r n a l | e x t e r n a l | s t a t i c | d y n a m i c | v i r t u a l | a b s t r a c t | c o n c r e t e | i n t e r f a c e | i m p l e m e n t a t i o n | s p e c i f i c a t i o n | d e f i n i t i o n | d e c l a r a t i o n | c o n f i g u r a t i o n | i n i t i a l i z a t i o n | f i n a l i z a t i o n | c l e a n u p | s e t u p | t e a r d o w n | s t a r t | s t o p | p a u s e | r e s u m e | r e s e t | r e f r e s h | r e l o a d | u p d a t e | u p g r a d e | d o w n g r a d e | m i g r a t e | r o l l b a c k | c o m m i t | r o l l b a c k | s a v e | l o a d | r e a d | w r i t e | e x e c u t e | r u n | i n v o k e | c a l l | s e n d | r e c e i v e | p u b l i s h | s u b s c r i b e | l i s t e n | w a t c h | o b s e r v e | t r i g g e r | e m i t | d i s p a t c h | b r o a d c a s t | n o t i f y | a l e r t | w a r n | i n f o | d e b u g | t r a c e | l o g | a u d i t | t r a c k | m o n i t o r | m e a s u r e | c o u n t | s u m | a v e r a g e | m i n | m a x | s o r t | f i l t e r | s e a r c h | f i n d | s e l e c t | i n s e r t | u p d a t e | d e l e t e | c r e a t e | d e s t r o y | b u i l d | c o m p i l e | p a r s e | r e n d e r | f o r m a t | v a l i d a t e | v e r i f y | c h e c k | t e s t | a s s e r t | e x p e c t | m o c k | s t u b | s p y | f a k e | d u m m y | p l a c e h o l d e r | t e m p l a t e | e x a m p l e | s a m p l e | d e m o | p r o t o t y p e | p r o o f | c o n c e p t | i d e a | p l a n | d e s i g n | a r c h i t e c t u r e | p a t t e r n | b e s t | p r a c t i c e | c o n v e n t i o n | s t a n d a r d | g u i d e l i n e | r u l e | p o l i c y | p r o c e d u r e | w o r k f l o w | p r o c e s s | s t e p | p h a s e | s t a g e | c y c l e | i t e r a t i o n | l o o p | c o n d i t i o n | b r a n c h | m e r g e | s p l i t | j o i n | f o r k | c l o n e | c o p y | m o v e | r e n a m e | r e p l a c e | s w a p | e x c h a n g e | c o n v e r t | t r a n s f o r m | m a p | r e d u c e | f i l t e r | f o l d | z i p | u n z i p | p a c k | u n p a c k | s e r i a l i z e | d e s e r i a l i z e | e n c o d e | d e c o d e | e n c r y p t | d e c r y p t | h a s h | s a l t | t o k e n | k e y | s e c r e t | p a s s w o r d | u s e r n a m e | e m a i l | p h o n e | a d d r e s s | n a m e | t i t l e | d e s c r i p t i o n | c o m m e n t | n o t e | t a g | l a b e l | c a t e g o r y | t y p e | k i n d | c l a s s | g r o u p | s e t | l i s t | a r r a y | m a p | d i c t | h a s h | t r e e | g r a p h | n o d e | e d g e | l i n k | p a t h | r o u t e | u r l | u r i | e n d p o i n t | r e s o u r c e | e n t i t y | o b j e c t | v a l u e | p r o p e r t y | a t t r i b u t e | f i e l d | c o l u m n | r o w | r e c o r d | d o c u m e n t | f i l e | f o l d e r | d i r e c t o r y | p r o j e c t | w o r k s p a c e | e n v i r o n m e n t | c o n t e x t | s c o p e | n a m e s p a c e | p a c k a g e | m o d u l e | l i b r a r y | f r a m e w o r k | t o o l | u t i l i t y | s e r v i c e | c o m p o n e n t | w i d g e t | c o n t r o l | e l e m e n t | i t e m | e n t i t y | m o d e l | v i e w | c o n t r o l l e r | p r e s e n t e r | v i e w m o d e l | a d a p t e r | w r a p p e r | p r o x y | f a c a d e | d e c o r a t o r | o b s e r v e r | l i s t e n e r | h a n d l e r | c a l l b a c k | p r o m i s e | f u t u r e | t a s k | j o b | w o r k e r | t h r e a d | p r o c e s s | q u e u e | s t a c k | h e a p | m e m o r y | s t o r a g e | d a t a b a s e | c a c h e | s e s s i o n | c o o k i e | t o k e n | a u t h o r i z a t i o n | a u t h e n t i c a t i o n | p e r m i s s i o n | r o l e | u s e r | a d m i n | g u e s t | a n o n y m o u s | p u b l i c | p r i v a t e | p r o t e c t e d | i n t e r n a l | e x t e r n a l | r e a d o n l y | w r i t e o n l y | r e a d w r i t e | i m m u t a b l e | m u t a b l e | c o n s t | v a r | l e t | f i n a l | s t a t i c | a b s t r a c t | v i r t u a l | o v e r r i d e | i m p l e m e n t | e x t e n d | i n h e r i t | c o m p o s e | m i x i n | t r a i t | i n t e r f a c e | c l a s s | s t r u c t | e n u m | u n i o n | t u p l e | r e c o r d | g e n e r i c | t e m p l a t e | m a c r o | a n n o t a t i o n | a t t r i b u t e | m e t a d a t a | r e f l e c t i o n | i n t r o s p e c t i o n | s e r i a l i z a t i o n | d e s e r i a l i z a t i o n | m a r s h a l l i n g | u n m a r s h a l l i n g | e n c o d i n g | d e c o d i n g | c o m p r e s s i o n | d e c o m p r e s s i o n | o p t i m i z a t i o n | p e r f o r m a n c e | s c a l a b i l i t y | r e l i a b i l i t y | a v a i l a b i l i t y | c o n s i s t e n c y | d u r a b i l i t y | s e c u r i t y | p r i v a c y | c o m p l i a n c e | g o v e r n a n c e | m o n i t o r i n g | l o g g i n g | d e b u g g i n g | p r o f i l i n g | t e s t i n g | v a l i d a t i o n | v e r i f i c a t i o n | d o c u m e n t a t i o n | s p e c i f i c a t i o n | r e q u i r e m e n t | d e s i g n | i m p l e m e n t a t i o n | d e p l o y m e n t | m a i n t e n a n c e | s u p p o r t | t r o u b l e s h o o t i n g | d e b u g g i n g | o p t i m i z a t i o n | r e f a c t o r i n g | m i g r a t i o n | u p g r a d e | d e p r e c a t i o n | r e t i r e m e n t | s u n s e t t i n g ) \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