@@ -41,6 +41,15 @@ function initCollaborativeWorkspace(io) {
4141 canvasHistory : [ ] ,
4242 cleanupTimer : null
4343 } ;
44+
45+ // Add virtual AI partner to room users list
46+ workspace . users . set ( 'forgeai' , {
47+ socketId : 'forgeai' ,
48+ username : 'ForgeAI (AI Partner)' ,
49+ avatarColor : '#8b5cf6' ,
50+ cursor : null
51+ } ) ;
52+
4453 workspaces . set ( roomId , workspace ) ;
4554 }
4655
@@ -50,7 +59,9 @@ function initCollaborativeWorkspace(io) {
5059 console . log ( `Cancelled cleanup timer for room: ${ roomId } ` ) ;
5160 }
5261
53- if ( workspace . users . size >= MAX_USERS_PER_ROOM ) {
62+ // Count human users (exclude 'forgeai')
63+ const humanCount = Array . from ( workspace . users . values ( ) ) . filter ( u => u . socketId !== 'forgeai' ) . length ;
64+ if ( humanCount >= MAX_USERS_PER_ROOM ) {
5465 socket . emit ( 'room-error' , 'Room is full (Maximum 5 users).' ) ;
5566 return ;
5667 }
@@ -145,6 +156,11 @@ function initCollaborativeWorkspace(io) {
145156 }
146157
147158 io . to ( roomId ) . emit ( 'new-message' , messageObj ) ;
159+
160+ // Trigger ForgeAI if message targets the agent
161+ if ( text && text . trim ( ) . toLowerCase ( ) . startsWith ( '@forgeai' ) ) {
162+ handleForgeAIMessage ( roomId , io , text ) ;
163+ }
148164 } ) ;
149165
150166 // Live shared AI assistant stream
@@ -341,8 +357,9 @@ Provide a concise, expert answer. If correcting code, explain the bug briefly an
341357 workspace . users . delete ( socket . id ) ;
342358 io . to ( roomId ) . emit ( 'user-left' , socket . id ) ;
343359
344- // If room is empty, trigger cleanup grace period
345- if ( workspace . users . size === 0 ) {
360+ // If room is empty of human users, trigger cleanup grace period
361+ const humanCount = Array . from ( workspace . users . values ( ) ) . filter ( u => u . socketId !== 'forgeai' ) . length ;
362+ if ( humanCount === 0 ) {
346363 workspace . cleanupTimer = setTimeout ( ( ) => {
347364 workspaces . delete ( roomId ) ;
348365 console . log ( `🧹 Room ${ roomId } has been deleted from memory (empty and inactive).` ) ;
@@ -352,4 +369,148 @@ Provide a concise, expert answer. If correcting code, explain the bug briefly an
352369 } ) ;
353370}
354371
372+ // Helper for parsing JSON safely from LLM response
373+ function parseJsonResponse ( text ) {
374+ let cleanText = text . trim ( ) ;
375+ if ( cleanText . startsWith ( '```json' ) ) {
376+ cleanText = cleanText . substring ( 7 ) ;
377+ } else if ( cleanText . startsWith ( '```' ) ) {
378+ cleanText = cleanText . substring ( 3 ) ;
379+ }
380+ if ( cleanText . endsWith ( '```' ) ) {
381+ cleanText = cleanText . substring ( 0 , cleanText . length - 3 ) ;
382+ }
383+ cleanText = cleanText . trim ( ) ;
384+
385+ try {
386+ return JSON . parse ( cleanText ) ;
387+ } catch ( e ) {
388+ const firstBrace = cleanText . indexOf ( '{' ) ;
389+ const lastBrace = cleanText . lastIndexOf ( '}' ) ;
390+ if ( firstBrace !== - 1 && lastBrace !== - 1 ) {
391+ const jsonSubstring = cleanText . substring ( firstBrace , lastBrace + 1 ) ;
392+ return JSON . parse ( jsonSubstring ) ;
393+ }
394+ throw e ;
395+ }
396+ }
397+
398+ // Background handler for ForgeAI interaction
399+ async function handleForgeAIMessage ( roomId , io , text ) {
400+ const workspace = workspaces . get ( roomId ) ;
401+ if ( ! workspace ) return ;
402+
403+ const promptText = text . replace ( / ^ @ f o r g e a i / i, '' ) . trim ( ) ;
404+ if ( ! promptText ) return ;
405+
406+ // 1. Emit a temporary typing message
407+ const aiTempMsgId = `msg-forgeai-loading-${ Date . now ( ) } ` ;
408+ const tempMessageObj = {
409+ id : aiTempMsgId ,
410+ sender : 'ForgeAI (AI Partner)' ,
411+ avatarColor : '#8b5cf6' ,
412+ text : '🤖 ForgeAI is typing...' ,
413+ timestamp : new Date ( ) . toISOString ( )
414+ } ;
415+ io . to ( roomId ) . emit ( 'new-message' , tempMessageObj ) ;
416+
417+ try {
418+ const geminiApiKey = process . env . GEMINI_API_KEY ;
419+ if ( ! geminiApiKey ) {
420+ throw new Error ( "Gemini API key is not configured on the server." ) ;
421+ }
422+
423+ const currentCode = workspace . yDoc ? workspace . yDoc . getText ( 'monaco' ) . toString ( ) : '' ;
424+
425+ const systemPrompt = `You are "ForgeAI", an autonomous AI programming partner participating in a collaborative room.
426+ You are helping developers with code. You can either just chat/explain, or edit the shared code document directly.
427+ If they ask you to write, modify, refactor, add, fix, or optimize code, you should perform an "edit". Otherwise, perform a "chat".
428+
429+ Current Code in the Editor:
430+ ${ currentCode }
431+
432+ User request: "${ promptText } "
433+
434+ You must respond with ONLY a valid JSON object. Do not include markdown code block backticks around the JSON.
435+ Schema:
436+ {
437+ "action": "chat" | "edit",
438+ "message": "Your conversational response/explanation to the developers in the chat.",
439+ "code": "If action is 'edit', the COMPLETE modified code content. If action is 'chat', leave this empty."
440+ }
441+ ` ;
442+
443+ const genAI = new GoogleGenerativeAI ( geminiApiKey ) ;
444+ const model = genAI . getGenerativeModel ( { model : 'gemini-2.5-flash' } ) ;
445+ const result = await model . generateContent ( systemPrompt ) ;
446+ const responseText = result . response . text ( ) ;
447+
448+ // Parse response
449+ let parsed ;
450+ try {
451+ parsed = parseJsonResponse ( responseText ) ;
452+ } catch ( err ) {
453+ console . error ( "Failed to parse ForgeAI response:" , responseText , err ) ;
454+ parsed = {
455+ action : "chat" ,
456+ message : parsed ?. message || responseText ,
457+ code : ""
458+ } ;
459+ }
460+
461+ // Delete temporary loading message
462+ io . to ( roomId ) . emit ( 'delete-message' , aiTempMsgId ) ;
463+
464+ // Send final message to chat
465+ const finalMsgObj = {
466+ id : `msg-forgeai-${ Date . now ( ) } ` ,
467+ sender : 'ForgeAI (AI Partner)' ,
468+ avatarColor : '#8b5cf6' ,
469+ text : parsed . message || 'I have completed your request.' ,
470+ timestamp : new Date ( ) . toISOString ( )
471+ } ;
472+ workspace . messages . push ( finalMsgObj ) ;
473+ if ( workspace . messages . length > MAX_MESSAGE_HISTORY ) {
474+ workspace . messages . shift ( ) ;
475+ }
476+ io . to ( roomId ) . emit ( 'new-message' , finalMsgObj ) ;
477+
478+ // If edit action, apply changes to Yjs doc
479+ if ( parsed . action === 'edit' && parsed . code ) {
480+ const textType = workspace . yDoc . getText ( 'monaco' ) ;
481+ workspace . yDoc . transact ( ( ) => {
482+ textType . delete ( 0 , textType . length ) ;
483+ textType . insert ( 0 , parsed . code ) ;
484+ } ) ;
485+
486+ const update = Y . encodeStateAsUpdate ( workspace . yDoc ) ;
487+ io . to ( roomId ) . emit ( 'yjs-update' , Array . from ( update ) ) ;
488+
489+ // Emit a system notification in the chat
490+ const sysMsg = {
491+ id : `msg-sys-${ Date . now ( ) } ` ,
492+ sender : 'System' ,
493+ avatarColor : '#94a3b8' ,
494+ text : `⚙️ ForgeAI has updated the active code buffer.` ,
495+ timestamp : new Date ( ) . toISOString ( )
496+ } ;
497+ workspace . messages . push ( sysMsg ) ;
498+ if ( workspace . messages . length > MAX_MESSAGE_HISTORY ) {
499+ workspace . messages . shift ( ) ;
500+ }
501+ io . to ( roomId ) . emit ( 'new-message' , sysMsg ) ;
502+ }
503+ } catch ( error ) {
504+ console . error ( "ForgeAI Agent Error:" , error ) ;
505+ io . to ( roomId ) . emit ( 'delete-message' , aiTempMsgId ) ;
506+ io . to ( roomId ) . emit ( 'new-message' , {
507+ id : `msg-forgeai-err-${ Date . now ( ) } ` ,
508+ sender : 'ForgeAI (AI Partner)' ,
509+ avatarColor : '#8b5cf6' ,
510+ text : `⚠️ Sorry, I encountered an error: ${ error . message } ` ,
511+ timestamp : new Date ( ) . toISOString ( )
512+ } ) ;
513+ }
514+ }
515+
355516module . exports = initCollaborativeWorkspace ;
0 commit comments