@@ -179,3 +179,354 @@ function isJSONString(str) {
179
179
return false ;
180
180
}
181
181
}
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