Spaces:
Build error
Build error
/** | |
* @license | |
* SPDX-License-Identifier: Apache-2.0 | |
*/ | |
/* tslint:disable */ | |
import React, {useCallback, useEffect, useState} from 'react'; | |
import {GeneratedContent} from './components/GeneratedContent'; | |
import {Icon} from './components/Icon'; | |
import {ParametersPanel} from './components/ParametersPanel'; | |
import {Window} from './components/Window'; | |
import {APP_DEFINITIONS_CONFIG, INITIAL_MAX_HISTORY_LENGTH} from './constants'; | |
import {streamAppContent} from './services/geminiService'; | |
import {AppDefinition, InteractionData} from './types'; | |
const DesktopView: React.FC<{onAppOpen: (app: AppDefinition) => void}> = ({ | |
onAppOpen, | |
}) => ( | |
<div className="flex flex-wrap content-start p-4"> | |
{APP_DEFINITIONS_CONFIG.map((app) => ( | |
<Icon key={app.id} app={app} onInteract={() => onAppOpen(app)} /> | |
))} | |
</div> | |
); | |
const App: React.FC = () => { | |
const [activeApp, setActiveApp] = useState<AppDefinition | null>(null); | |
const [previousActiveApp, setPreviousActiveApp] = | |
useState<AppDefinition | null>(null); | |
const [llmContent, setLlmContent] = useState<string>(''); | |
const [isLoading, setIsLoading] = useState<boolean>(false); | |
const [error, setError] = useState<string | null>(null); | |
const [interactionHistory, setInteractionHistory] = useState< | |
InteractionData[] | |
>([]); | |
const [isParametersOpen, setIsParametersOpen] = useState<boolean>(false); | |
const [currentMaxHistoryLength, setCurrentMaxHistoryLength] = | |
useState<number>(INITIAL_MAX_HISTORY_LENGTH); | |
// Statefulness feature state | |
const [isStatefulnessEnabled, setIsStatefulnessEnabled] = | |
useState<boolean>(false); | |
const [appContentCache, setAppContentCache] = useState< | |
Record<string, string> | |
>({}); | |
const [currentAppPath, setCurrentAppPath] = useState<string[]>([]); // For UI graph statefulness | |
const internalHandleLlmRequest = useCallback( | |
async (historyForLlm: InteractionData[], maxHistoryLength: number) => { | |
if (historyForLlm.length === 0) { | |
setError('No interaction data to process.'); | |
return; | |
} | |
setIsLoading(true); | |
setError(null); | |
let accumulatedContent = ''; | |
// Clear llmContent before streaming new content only if not loading from cache | |
// This is now handled before this function is called (in handleAppOpen/handleInteraction) | |
// setLlmContent(''); // Removed from here, set by caller if needed | |
try { | |
const stream = streamAppContent(historyForLlm, maxHistoryLength); | |
for await (const chunk of stream) { | |
accumulatedContent += chunk; | |
setLlmContent((prev) => prev + chunk); | |
} | |
} catch (e: any) { | |
setError('Failed to stream content from the API.'); | |
console.error(e); | |
accumulatedContent = `<div class="p-4 text-red-600 bg-red-100 rounded-md">Error loading content.</div>`; | |
setLlmContent(accumulatedContent); | |
} finally { | |
setIsLoading(false); | |
// Caching logic is now in useEffect watching llmContent, isLoading, activeApp, currentAppPath etc. | |
} | |
}, | |
[], | |
); | |
// Effect to cache content when loading finishes and statefulness is enabled | |
useEffect(() => { | |
if ( | |
!isLoading && | |
currentAppPath.length > 0 && | |
isStatefulnessEnabled && | |
llmContent | |
) { | |
const cacheKey = currentAppPath.join('__'); | |
// Update cache if content is different or not yet cached for this path | |
if (appContentCache[cacheKey] !== llmContent) { | |
setAppContentCache((prevCache) => ({ | |
...prevCache, | |
[cacheKey]: llmContent, | |
})); | |
} | |
} | |
}, [ | |
llmContent, | |
isLoading, | |
currentAppPath, | |
isStatefulnessEnabled, | |
appContentCache, | |
]); | |
const handleInteraction = useCallback( | |
async (interactionData: InteractionData) => { | |
if (interactionData.id === 'app_close_button') { | |
// This specific ID might not be generated by LLM | |
handleCloseAppView(); // Use existing close logic | |
return; | |
} | |
const newHistory = [ | |
interactionData, | |
...interactionHistory.slice(0, currentMaxHistoryLength - 1), | |
]; | |
setInteractionHistory(newHistory); | |
const newPath = activeApp | |
? [...currentAppPath, interactionData.id] | |
: [interactionData.id]; | |
setCurrentAppPath(newPath); | |
const cacheKey = newPath.join('__'); | |
setLlmContent(''); | |
setError(null); | |
if (isStatefulnessEnabled && appContentCache[cacheKey]) { | |
setLlmContent(appContentCache[cacheKey]); | |
setIsLoading(false); | |
} else { | |
internalHandleLlmRequest(newHistory, currentMaxHistoryLength); | |
} | |
}, | |
[ | |
interactionHistory, | |
internalHandleLlmRequest, | |
activeApp, | |
currentMaxHistoryLength, | |
currentAppPath, | |
isStatefulnessEnabled, | |
appContentCache, | |
], | |
); | |
const handleAppOpen = (app: AppDefinition) => { | |
const initialInteraction: InteractionData = { | |
id: app.id, | |
type: 'app_open', | |
elementText: app.name, | |
elementType: 'icon', | |
appContext: app.id, | |
}; | |
const newHistory = [initialInteraction]; | |
setInteractionHistory(newHistory); | |
const appPath = [app.id]; | |
setCurrentAppPath(appPath); | |
const cacheKey = appPath.join('__'); | |
if (isParametersOpen) { | |
setIsParametersOpen(false); | |
} | |
setActiveApp(app); | |
setLlmContent(''); | |
setError(null); | |
if (isStatefulnessEnabled && appContentCache[cacheKey]) { | |
setLlmContent(appContentCache[cacheKey]); | |
setIsLoading(false); | |
} else { | |
internalHandleLlmRequest(newHistory, currentMaxHistoryLength); | |
} | |
}; | |
const handleCloseAppView = () => { | |
setActiveApp(null); | |
setLlmContent(''); | |
setError(null); | |
setInteractionHistory([]); | |
setCurrentAppPath([]); | |
}; | |
const handleToggleParametersPanel = () => { | |
setIsParametersOpen((prevIsOpen) => { | |
const nowOpeningParameters = !prevIsOpen; | |
if (nowOpeningParameters) { | |
// Store the currently active app (if any) so it can be restored, | |
// or null if no app is active (desktop view). | |
setPreviousActiveApp(activeApp); | |
setActiveApp(null); // Clear active app to show parameters panel | |
setLlmContent(''); | |
setError(null); | |
// Interaction history and current path are not cleared here, | |
// as they might be relevant if the user returns to an app. | |
} else { | |
// Closing parameters panel - always go back to desktop view | |
setPreviousActiveApp(null); // Clear any stored previous app | |
setActiveApp(null); // Ensure desktop view | |
setLlmContent(''); | |
setError(null); | |
setInteractionHistory([]); // Clear history when returning to desktop from parameters | |
setCurrentAppPath([]); // Clear app path | |
} | |
return nowOpeningParameters; | |
}); | |
}; | |
const handleUpdateHistoryLength = (newLength: number) => { | |
setCurrentMaxHistoryLength(newLength); | |
// Trim interaction history if new length is shorter | |
setInteractionHistory((prev) => prev.slice(0, newLength)); | |
}; | |
const handleSetStatefulness = (enabled: boolean) => { | |
setIsStatefulnessEnabled(enabled); | |
if (!enabled) { | |
setAppContentCache({}); | |
} | |
}; | |
const windowTitle = isParametersOpen | |
? 'Gemini Computer' | |
: activeApp | |
? activeApp.name | |
: 'Gemini Computer'; | |
const contentBgColor = '#ffffff'; | |
const handleMasterClose = () => { | |
if (isParametersOpen) { | |
handleToggleParametersPanel(); | |
} else if (activeApp) { | |
handleCloseAppView(); | |
} | |
}; | |
return ( | |
<div className="bg-white w-full min-h-screen flex items-center justify-center p-4"> | |
<Window | |
title={windowTitle} | |
onClose={handleMasterClose} | |
isAppOpen={!!activeApp && !isParametersOpen} | |
appId={activeApp?.id} | |
onToggleParameters={handleToggleParametersPanel} | |
onExitToDesktop={handleCloseAppView} | |
isParametersPanelOpen={isParametersOpen}> | |
<div | |
className="w-full h-full" | |
style={{backgroundColor: contentBgColor}}> | |
{isParametersOpen ? ( | |
<ParametersPanel | |
currentLength={currentMaxHistoryLength} | |
onUpdateHistoryLength={handleUpdateHistoryLength} | |
onClosePanel={handleToggleParametersPanel} | |
isStatefulnessEnabled={isStatefulnessEnabled} | |
onSetStatefulness={handleSetStatefulness} | |
/> | |
) : !activeApp ? ( | |
<DesktopView onAppOpen={handleAppOpen} /> | |
) : ( | |
<> | |
{isLoading && llmContent.length === 0 && ( | |
<div className="flex justify-center items-center h-full"> | |
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div> | |
</div> | |
)} | |
{error && ( | |
<div className="p-4 text-red-600 bg-red-100 rounded-md"> | |
{error} | |
</div> | |
)} | |
{(!isLoading || llmContent) && ( | |
<GeneratedContent | |
htmlContent={llmContent} | |
onInteract={handleInteraction} | |
appContext={activeApp.id} | |
isLoading={isLoading} | |
/> | |
)} | |
</> | |
)} | |
</div> | |
</Window> | |
</div> | |
); | |
}; | |
export default App; | |