mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-19 15:14:21 +01:00
refactor: Component hierarchy and code cleanup (#1319)
This commit is contained in:
@@ -1,5 +0,0 @@
|
|||||||
Put `goosey` in your $PATH if you want to launch via:
|
|
||||||
|
|
||||||
`goosey .`
|
|
||||||
|
|
||||||
Will open goose GUI from any path you specify
|
|
||||||
9
ui/desktop/scripts/README.md
Normal file
9
ui/desktop/scripts/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Goosey
|
||||||
|
|
||||||
|
Put `goosey` in your $PATH if you want to launch via:
|
||||||
|
|
||||||
|
```
|
||||||
|
goosey .
|
||||||
|
```
|
||||||
|
|
||||||
|
This will open goose GUI from any path you specify
|
||||||
@@ -1,64 +1,146 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { addExtensionFromDeepLink } from './extensions';
|
import { addExtensionFromDeepLink } from './extensions';
|
||||||
import LauncherWindow from './LauncherWindow';
|
import { getStoredModel } from './utils/providerUtils';
|
||||||
import ChatWindow from './ChatWindow';
|
import { getStoredProvider, initializeSystem } from './utils/providerUtils';
|
||||||
|
import { useModel } from './components/settings/models/ModelContext';
|
||||||
|
import { useRecentModels } from './components/settings/models/RecentModels';
|
||||||
|
import { createSelectedModel } from './components/settings/models/utils';
|
||||||
|
import { getDefaultModel } from './components/settings/models/hardcoded_stuff';
|
||||||
import ErrorScreen from './components/ErrorScreen';
|
import ErrorScreen from './components/ErrorScreen';
|
||||||
import { ConfirmationModal } from './components/ui/ConfirmationModal';
|
import { ConfirmationModal } from './components/ui/ConfirmationModal';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
import { ModelProvider } from './components/settings/models/ModelContext';
|
|
||||||
import { ActiveKeysProvider } from './components/settings/api_keys/ActiveKeysContext';
|
|
||||||
import { extractExtensionName } from './components/settings/extensions/utils';
|
import { extractExtensionName } from './components/settings/extensions/utils';
|
||||||
|
|
||||||
|
import WelcomeView from './components/WelcomeView';
|
||||||
|
import ChatView from './components/ChatView';
|
||||||
|
import SettingsView from './components/settings/SettingsView';
|
||||||
|
import MoreModelsView from './components/settings/models/MoreModelsView';
|
||||||
|
import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView';
|
||||||
|
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
|
export type View =
|
||||||
|
| 'welcome'
|
||||||
|
| 'chat'
|
||||||
|
| 'settings'
|
||||||
|
| 'moreModels'
|
||||||
|
| 'configureProviders'
|
||||||
|
| 'configPage';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [fatalError, setFatalError] = useState<string | null>(null);
|
const [fatalError, setFatalError] = useState<string | null>(null);
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [pendingLink, setPendingLink] = useState<string | null>(null);
|
const [pendingLink, setPendingLink] = useState<string | null>(null);
|
||||||
const [modalMessage, setModalMessage] = useState<string>('');
|
const [modalMessage, setModalMessage] = useState<string>('');
|
||||||
const [isInstalling, setIsInstalling] = useState(false); // Track installation progress
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const [view, setView] = useState<View>('welcome');
|
||||||
const isLauncher = searchParams.get('window') === 'launcher';
|
const { switchModel } = useModel();
|
||||||
const navigate = () => console.log('todo - bring back nav');
|
const { addRecentModel } = useRecentModels();
|
||||||
|
|
||||||
// Utility function to extract the command from the link
|
// Utility function to extract the command from the link
|
||||||
function extractCommand(link: string): string {
|
function extractCommand(link: string): string {
|
||||||
const url = new URL(link);
|
const url = new URL(link);
|
||||||
const cmd = url.searchParams.get('cmd') || 'Unknown Command';
|
const cmd = url.searchParams.get('cmd') || 'Unknown Command';
|
||||||
const args = url.searchParams.getAll('arg').map(decodeURIComponent);
|
const args = url.searchParams.getAll('arg').map(decodeURIComponent);
|
||||||
return `${cmd} ${args.join(' ')}`.trim(); // Combine the command and arguments
|
return `${cmd} ${args.join(' ')}`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleAddExtension = (_, link: string) => {
|
const handleAddExtension = (_: any, link: string) => {
|
||||||
const command = extractCommand(link); // Extract and format the command
|
const command = extractCommand(link);
|
||||||
const extName = extractExtensionName(link);
|
const extName = extractExtensionName(link);
|
||||||
window.electron.logInfo(`Adding extension from deep link ${link}`);
|
window.electron.logInfo(`Adding extension from deep link ${link}`);
|
||||||
setPendingLink(link); // Save the link for later use
|
setPendingLink(link);
|
||||||
setModalMessage(
|
setModalMessage(
|
||||||
`Are you sure you want to install the ${extName} extension?\n\nCommand: ${command}`
|
`Are you sure you want to install the ${extName} extension?\n\nCommand: ${command}`
|
||||||
); // Display command
|
);
|
||||||
setModalVisible(true); // Show confirmation modal
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.electron.on('add-extension', handleAddExtension);
|
window.electron.on('add-extension', handleAddExtension);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Clean up the event listener when the component unmounts
|
|
||||||
window.electron.off('add-extension', handleAddExtension);
|
window.electron.off('add-extension', handleAddExtension);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Keyboard shortcut handler
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === 'n') {
|
||||||
|
event.preventDefault();
|
||||||
|
window.electron.createChatWindow();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Attempt to detect config for a stored provider
|
||||||
|
useEffect(() => {
|
||||||
|
const config = window.electron.getConfig();
|
||||||
|
const storedProvider = getStoredProvider(config);
|
||||||
|
if (storedProvider) {
|
||||||
|
setView('chat');
|
||||||
|
} else {
|
||||||
|
setView('welcome');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Initialize system if we have a stored provider
|
||||||
|
useEffect(() => {
|
||||||
|
const setupStoredProvider = async () => {
|
||||||
|
const config = window.electron.getConfig();
|
||||||
|
|
||||||
|
if (config.GOOSE_PROVIDER && config.GOOSE_MODEL) {
|
||||||
|
window.electron.logInfo(
|
||||||
|
'Initializing system with environment: GOOSE_MODEL and GOOSE_PROVIDER as priority.'
|
||||||
|
);
|
||||||
|
await initializeSystem(config.GOOSE_PROVIDER, config.GOOSE_MODEL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const storedProvider = getStoredProvider(config);
|
||||||
|
const storedModel = getStoredModel();
|
||||||
|
if (storedProvider) {
|
||||||
|
try {
|
||||||
|
await initializeSystem(storedProvider, storedModel);
|
||||||
|
|
||||||
|
if (!storedModel) {
|
||||||
|
const modelName = getDefaultModel(storedProvider.toLowerCase());
|
||||||
|
const model = createSelectedModel(storedProvider.toLowerCase(), modelName);
|
||||||
|
switchModel(model);
|
||||||
|
addRecentModel(model);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize with stored provider:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setupStoredProvider();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleFatalError = (_: any, errorMessage: string) => {
|
||||||
|
setFatalError(errorMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.electron.on('fatal-error', handleFatalError);
|
||||||
|
return () => {
|
||||||
|
window.electron.off('fatal-error', handleFatalError);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
if (pendingLink && !isInstalling) {
|
if (pendingLink && !isInstalling) {
|
||||||
setIsInstalling(true); // Disable further attempts
|
setIsInstalling(true);
|
||||||
console.log('Confirming installation for link:', pendingLink);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addExtensionFromDeepLink(pendingLink, navigate);
|
await addExtensionFromDeepLink(pendingLink, setView);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to add extension:', error);
|
console.error('Failed to add extension:', error);
|
||||||
} finally {
|
} finally {
|
||||||
// Always reset states
|
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
setPendingLink(null);
|
setPendingLink(null);
|
||||||
setIsInstalling(false);
|
setIsInstalling(false);
|
||||||
@@ -69,28 +151,22 @@ export default function App() {
|
|||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
console.log('Cancelled extension installation.');
|
console.log('Cancelled extension installation.');
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
setPendingLink(null); // Clear the link if the user cancels
|
setPendingLink(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleFatalError = (_: any, errorMessage: string) => {
|
|
||||||
setFatalError(errorMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Listen for fatal errors from main process
|
|
||||||
window.electron.on('fatal-error', handleFatalError);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.electron.off('fatal-error', handleFatalError);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (fatalError) {
|
if (fatalError) {
|
||||||
return <ErrorScreen error={fatalError} onReload={() => window.electron.reloadApp()} />;
|
return <ErrorScreen error={fatalError} onReload={() => window.electron.reloadApp()} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<ToastContainer
|
||||||
|
aria-label="Toast notifications"
|
||||||
|
position="top-right"
|
||||||
|
autoClose={3000}
|
||||||
|
closeOnClick
|
||||||
|
pauseOnHover
|
||||||
|
/>
|
||||||
{modalVisible && (
|
{modalVisible && (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={modalVisible}
|
isOpen={modalVisible}
|
||||||
@@ -101,18 +177,42 @@ export default function App() {
|
|||||||
isSubmitting={isInstalling}
|
isSubmitting={isInstalling}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ModelProvider>
|
<div className="relative w-screen h-screen overflow-hidden bg-bgApp flex flex-col">
|
||||||
<ActiveKeysProvider>
|
<div className="titlebar-drag-region" />
|
||||||
{isLauncher ? <LauncherWindow /> : <ChatWindow />}
|
<div>
|
||||||
<ToastContainer
|
{view === 'welcome' && (
|
||||||
aria-label="Toast notifications"
|
<WelcomeView
|
||||||
position="top-right"
|
onSubmit={() => {
|
||||||
autoClose={3000}
|
setView('chat');
|
||||||
closeOnClick
|
}}
|
||||||
pauseOnHover
|
|
||||||
/>
|
/>
|
||||||
</ActiveKeysProvider>
|
)}
|
||||||
</ModelProvider>
|
{view === 'settings' && (
|
||||||
|
<SettingsView
|
||||||
|
onClose={() => {
|
||||||
|
setView('chat');
|
||||||
|
}}
|
||||||
|
setView={setView}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{view === 'moreModels' && (
|
||||||
|
<MoreModelsView
|
||||||
|
onClose={() => {
|
||||||
|
setView('settings');
|
||||||
|
}}
|
||||||
|
setView={setView}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{view === 'configureProviders' && (
|
||||||
|
<ConfigureProvidersView
|
||||||
|
onClose={() => {
|
||||||
|
setView('settings');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{view === 'chat' && <ChatView setView={setView} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,414 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import { Message, useChat } from './ai-sdk-fork/useChat';
|
|
||||||
import { getApiUrl, getSecretKey } from './config';
|
|
||||||
import BottomMenu from './components/BottomMenu';
|
|
||||||
import FlappyGoose from './components/FlappyGoose';
|
|
||||||
import GooseMessage from './components/GooseMessage';
|
|
||||||
import Input from './components/Input';
|
|
||||||
import LoadingGoose from './components/LoadingGoose';
|
|
||||||
import MoreMenu from './components/MoreMenu';
|
|
||||||
import { Card } from './components/ui/card';
|
|
||||||
import { ScrollArea, ScrollAreaHandle } from './components/ui/scroll-area';
|
|
||||||
import UserMessage from './components/UserMessage';
|
|
||||||
import WingToWing, { Working } from './components/WingToWing';
|
|
||||||
import { askAi } from './utils/askAI';
|
|
||||||
import { getStoredModel, Provider } from './utils/providerUtils';
|
|
||||||
import { ChatLayout } from './components/chat_window/ChatLayout';
|
|
||||||
import { WelcomeScreen } from './components/welcome_screen/WelcomeScreen';
|
|
||||||
import { getStoredProvider, initializeSystem } from './utils/providerUtils';
|
|
||||||
import { useModel } from './components/settings/models/ModelContext';
|
|
||||||
import { useRecentModels } from './components/settings/models/RecentModels';
|
|
||||||
import { createSelectedModel } from './components/settings/models/utils';
|
|
||||||
import { getDefaultModel } from './components/settings/models/hardcoded_stuff';
|
|
||||||
import Splash from './components/Splash';
|
|
||||||
import Settings from './components/settings/Settings';
|
|
||||||
import MoreModelsSettings from './components/settings/models/MoreModels';
|
|
||||||
import ConfigureProviders from './components/settings/providers/ConfigureProviders';
|
|
||||||
import { ConfigPage } from './components/pages/ConfigPage';
|
|
||||||
|
|
||||||
export interface Chat {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
messages: Array<{
|
|
||||||
id: string;
|
|
||||||
role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool';
|
|
||||||
content: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type View =
|
|
||||||
| 'welcome'
|
|
||||||
| 'chat'
|
|
||||||
| 'settings'
|
|
||||||
| 'moreModels'
|
|
||||||
| 'configureProviders'
|
|
||||||
| 'configPage';
|
|
||||||
|
|
||||||
// This component is our main chat content.
|
|
||||||
// We'll move the majority of chat logic here, minus the 'view' state.
|
|
||||||
export function ChatContent({
|
|
||||||
chats,
|
|
||||||
setChats,
|
|
||||||
selectedChatId,
|
|
||||||
setSelectedChatId,
|
|
||||||
initialQuery,
|
|
||||||
setProgressMessage,
|
|
||||||
setWorking,
|
|
||||||
setView,
|
|
||||||
}: {
|
|
||||||
chats: Chat[];
|
|
||||||
setChats: React.Dispatch<React.SetStateAction<Chat[]>>;
|
|
||||||
selectedChatId: number;
|
|
||||||
setSelectedChatId: React.Dispatch<React.SetStateAction<number>>;
|
|
||||||
initialQuery: string | null;
|
|
||||||
setProgressMessage: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
setWorking: React.Dispatch<React.SetStateAction<Working>>;
|
|
||||||
setView: (view: View) => void;
|
|
||||||
}) {
|
|
||||||
const chat = chats.find((c: Chat) => c.id === selectedChatId);
|
|
||||||
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
|
|
||||||
const [hasMessages, setHasMessages] = useState(false);
|
|
||||||
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
|
|
||||||
const [showGame, setShowGame] = useState(false);
|
|
||||||
const [working, setWorkingLocal] = useState<Working>(Working.Idle);
|
|
||||||
const scrollRef = useRef<ScrollAreaHandle>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setWorking(working);
|
|
||||||
}, [working, setWorking]);
|
|
||||||
|
|
||||||
const updateWorking = (newWorking: Working) => {
|
|
||||||
setWorkingLocal(newWorking);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { messages, append, stop, isLoading, error, setMessages } = useChat({
|
|
||||||
api: getApiUrl('/reply'),
|
|
||||||
initialMessages: chat?.messages || [],
|
|
||||||
onToolCall: ({ toolCall }) => {
|
|
||||||
updateWorking(Working.Working);
|
|
||||||
setProgressMessage(`Executing tool: ${toolCall.toolName}`);
|
|
||||||
},
|
|
||||||
onResponse: (response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
setProgressMessage('An error occurred while receiving the response.');
|
|
||||||
updateWorking(Working.Idle);
|
|
||||||
} else {
|
|
||||||
setProgressMessage('thinking...');
|
|
||||||
updateWorking(Working.Working);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onFinish: async (message, _) => {
|
|
||||||
window.electron.stopPowerSaveBlocker();
|
|
||||||
setTimeout(() => {
|
|
||||||
setProgressMessage('Task finished. Click here to expand.');
|
|
||||||
updateWorking(Working.Idle);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const fetchResponses = await askAi(message.content);
|
|
||||||
setMessageMetadata((prev) => ({ ...prev, [message.id]: fetchResponses }));
|
|
||||||
|
|
||||||
const timeSinceLastInteraction = Date.now() - lastInteractionTime;
|
|
||||||
window.electron.logInfo('last interaction:' + lastInteractionTime);
|
|
||||||
if (timeSinceLastInteraction > 60000) {
|
|
||||||
// 60000ms = 1 minute
|
|
||||||
window.electron.showNotification({
|
|
||||||
title: 'Goose finished the task.',
|
|
||||||
body: 'Click here to expand.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update chat messages when they change
|
|
||||||
useEffect(() => {
|
|
||||||
const updatedChats = chats.map((c) => (c.id === selectedChatId ? { ...c, messages } : c));
|
|
||||||
setChats(updatedChats);
|
|
||||||
}, [messages, selectedChatId]);
|
|
||||||
|
|
||||||
const initialQueryAppended = useRef(false);
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialQuery && !initialQueryAppended.current) {
|
|
||||||
append({ role: 'user', content: initialQuery });
|
|
||||||
initialQueryAppended.current = true;
|
|
||||||
}
|
|
||||||
}, [initialQuery]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (messages.length > 0) {
|
|
||||||
setHasMessages(true);
|
|
||||||
}
|
|
||||||
}, [messages]);
|
|
||||||
|
|
||||||
// Handle submit
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
window.electron.startPowerSaveBlocker();
|
|
||||||
const customEvent = e as CustomEvent;
|
|
||||||
const content = customEvent.detail?.value || '';
|
|
||||||
if (content.trim()) {
|
|
||||||
setLastInteractionTime(Date.now());
|
|
||||||
append({
|
|
||||||
role: 'user',
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
if (scrollRef.current?.scrollToBottom) {
|
|
||||||
scrollRef.current.scrollToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.log('Error:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onStopGoose = () => {
|
|
||||||
stop();
|
|
||||||
setLastInteractionTime(Date.now());
|
|
||||||
window.electron.stopPowerSaveBlocker();
|
|
||||||
|
|
||||||
const lastMessage: Message = messages[messages.length - 1];
|
|
||||||
if (lastMessage.role === 'user' && lastMessage.toolInvocations === undefined) {
|
|
||||||
// Remove the last user message.
|
|
||||||
if (messages.length > 1) {
|
|
||||||
setMessages(messages.slice(0, -1));
|
|
||||||
} else {
|
|
||||||
setMessages([]);
|
|
||||||
}
|
|
||||||
} else if (lastMessage.role === 'assistant' && lastMessage.toolInvocations !== undefined) {
|
|
||||||
// Add messaging about interrupted ongoing tool invocations
|
|
||||||
const newLastMessage: Message = {
|
|
||||||
...lastMessage,
|
|
||||||
toolInvocations: lastMessage.toolInvocations.map((invocation) => {
|
|
||||||
if (invocation.state !== 'result') {
|
|
||||||
return {
|
|
||||||
...invocation,
|
|
||||||
result: [
|
|
||||||
{
|
|
||||||
audience: ['user'],
|
|
||||||
text: 'Interrupted.\n',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
audience: ['assistant'],
|
|
||||||
text: 'Interrupted by the user to make a correction.\n',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
state: 'result',
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return invocation;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedMessages = [...messages.slice(0, -1), newLastMessage];
|
|
||||||
setMessages(updatedMessages);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col w-full h-screen items-center justify-center">
|
|
||||||
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle border-b border-borderSubtle">
|
|
||||||
{/* Pass setView to MoreMenu so it can switch to settings or other views */}
|
|
||||||
<MoreMenu setView={setView} />
|
|
||||||
</div>
|
|
||||||
<Card className="flex flex-col flex-1 rounded-none h-[calc(100vh-95px)] w-full bg-bgApp mt-0 border-none relative">
|
|
||||||
{messages.length === 0 ? (
|
|
||||||
<Splash append={append} />
|
|
||||||
) : (
|
|
||||||
<ScrollArea ref={scrollRef} className="flex-1 px-4" autoScroll>
|
|
||||||
{messages.map((message) => (
|
|
||||||
<div key={message.id} className="mt-[16px]">
|
|
||||||
{message.role === 'user' ? (
|
|
||||||
<UserMessage message={message} />
|
|
||||||
) : (
|
|
||||||
<GooseMessage
|
|
||||||
message={message}
|
|
||||||
messages={messages}
|
|
||||||
metadata={messageMetadata[message.id]}
|
|
||||||
append={append}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{error && (
|
|
||||||
<div className="flex flex-col items-center justify-center p-4">
|
|
||||||
<div className="text-red-700 dark:text-red-300 bg-red-400/50 p-3 rounded-lg mb-2">
|
|
||||||
{error.message || 'Honk! Goose experienced an error while responding'}
|
|
||||||
{error.status && <span className="ml-2">(Status: {error.status})</span>}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="px-3 py-2 mt-2 text-center whitespace-nowrap cursor-pointer text-textStandard border border-borderSubtle hover:bg-bgSubtle rounded-full inline-block transition-all duration-150"
|
|
||||||
onClick={async () => {
|
|
||||||
const lastUserMessage = messages.reduceRight(
|
|
||||||
(found, m) => found || (m.role === 'user' ? m : null),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
if (lastUserMessage) {
|
|
||||||
append({
|
|
||||||
role: 'user',
|
|
||||||
content: lastUserMessage.content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Retry Last Message
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="block h-16" />
|
|
||||||
</ScrollArea>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
{isLoading && <LoadingGoose />}
|
|
||||||
<Input
|
|
||||||
handleSubmit={handleSubmit}
|
|
||||||
disabled={isLoading}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onStop={onStopGoose}
|
|
||||||
/>
|
|
||||||
<BottomMenu hasMessages={hasMessages} setView={setView} />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ChatWindow() {
|
|
||||||
// We'll add a state controlling which "view" is active.
|
|
||||||
const [view, setView] = useState<View>('welcome');
|
|
||||||
|
|
||||||
// Shared function to create a chat window
|
|
||||||
const openNewChatWindow = () => {
|
|
||||||
window.electron.createChatWindow();
|
|
||||||
};
|
|
||||||
const { switchModel } = useModel();
|
|
||||||
const { addRecentModel } = useRecentModels();
|
|
||||||
|
|
||||||
// This will store chat data for the "chat" view.
|
|
||||||
const [chats, setChats] = useState<Chat[]>(() => [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Chat 1',
|
|
||||||
messages: [],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const [selectedChatId, setSelectedChatId] = useState(1);
|
|
||||||
|
|
||||||
// Additional states
|
|
||||||
const [mode, setMode] = useState<'expanded' | 'compact'>('expanded');
|
|
||||||
const [working, setWorking] = useState<Working>(Working.Idle);
|
|
||||||
const [progressMessage, setProgressMessage] = useState<string>('');
|
|
||||||
const [initialQuery, setInitialQuery] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Keyboard shortcut handler
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if ((event.metaKey || event.ctrlKey) && event.key === 'n') {
|
|
||||||
event.preventDefault();
|
|
||||||
openNewChatWindow();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Attempt to detect config for a stored provider
|
|
||||||
useEffect(() => {
|
|
||||||
const config = window.electron.getConfig();
|
|
||||||
const storedProvider = getStoredProvider(config);
|
|
||||||
if (storedProvider) {
|
|
||||||
setView('chat');
|
|
||||||
} else {
|
|
||||||
setView('welcome');
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Initialize system if we have a stored provider
|
|
||||||
useEffect(() => {
|
|
||||||
const setupStoredProvider = async () => {
|
|
||||||
const config = window.electron.getConfig();
|
|
||||||
|
|
||||||
if (config.GOOSE_PROVIDER && config.GOOSE_MODEL) {
|
|
||||||
window.electron.logInfo(
|
|
||||||
'Initializing system with environment: GOOSE_MODEL and GOOSE_PROVIDER as priority.'
|
|
||||||
);
|
|
||||||
await initializeSystem(config.GOOSE_PROVIDER, config.GOOSE_MODEL);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const storedProvider = getStoredProvider(config);
|
|
||||||
const storedModel = getStoredModel();
|
|
||||||
if (storedProvider) {
|
|
||||||
try {
|
|
||||||
await initializeSystem(storedProvider, storedModel);
|
|
||||||
|
|
||||||
if (!storedModel) {
|
|
||||||
const modelName = getDefaultModel(storedProvider.toLowerCase());
|
|
||||||
const model = createSelectedModel(storedProvider.toLowerCase(), modelName);
|
|
||||||
switchModel(model);
|
|
||||||
addRecentModel(model);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize with stored provider:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setupStoredProvider();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChatLayout mode={mode}>
|
|
||||||
{/* Conditionally render based on `view` */}
|
|
||||||
{view === 'welcome' && (
|
|
||||||
<WelcomeScreen
|
|
||||||
onSubmit={() => {
|
|
||||||
setView('chat');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{view === 'settings' && (
|
|
||||||
<Settings
|
|
||||||
onClose={() => {
|
|
||||||
setView('chat');
|
|
||||||
}}
|
|
||||||
setView={setView}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{view === 'moreModels' && (
|
|
||||||
<MoreModelsSettings
|
|
||||||
onClose={() => {
|
|
||||||
setView('settings');
|
|
||||||
}}
|
|
||||||
setView={setView}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{view === 'configureProviders' && (
|
|
||||||
<ConfigureProviders
|
|
||||||
onClose={() => {
|
|
||||||
setView('settings');
|
|
||||||
}}
|
|
||||||
setView={setView}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{view === 'chat' && (
|
|
||||||
<ChatContent
|
|
||||||
chats={chats}
|
|
||||||
setChats={setChats}
|
|
||||||
selectedChatId={selectedChatId}
|
|
||||||
setSelectedChatId={setSelectedChatId}
|
|
||||||
initialQuery={initialQuery}
|
|
||||||
setProgressMessage={setProgressMessage}
|
|
||||||
setWorking={setWorking}
|
|
||||||
setView={setView}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ChatLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
|
||||||
|
|
||||||
export default function SpotlightWindow() {
|
|
||||||
const [query, setQuery] = useState('');
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (query.trim()) {
|
|
||||||
// Create a new chat window with the query
|
|
||||||
window.electron.createChatWindow(query);
|
|
||||||
setQuery('');
|
|
||||||
inputRef.current.blur();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-screen w-screen flex items-center justify-center bg-transparent overflow-hidden">
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
className="w-[600px] bg-white/80 backdrop-blur-lg rounded-lg shadow-lg p-4"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
|
||||||
value={query}
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
className="w-full bg-transparent text-black text-xl px-4 py-2 outline-none placeholder-gray-400"
|
|
||||||
placeholder="Type a command..."
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
209
ui/desktop/src/components/ChatView.tsx
Normal file
209
ui/desktop/src/components/ChatView.tsx
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Message, useChat } from '../ai-sdk-fork/useChat';
|
||||||
|
import { getApiUrl } from '../config';
|
||||||
|
import BottomMenu from './BottomMenu';
|
||||||
|
import FlappyGoose from './FlappyGoose';
|
||||||
|
import GooseMessage from './GooseMessage';
|
||||||
|
import Input from './Input';
|
||||||
|
import { type View } from '../App';
|
||||||
|
import LoadingGoose from './LoadingGoose';
|
||||||
|
import MoreMenu from './MoreMenu';
|
||||||
|
import { Card } from './ui/card';
|
||||||
|
import { ScrollArea, ScrollAreaHandle } from './ui/scroll-area';
|
||||||
|
import UserMessage from './UserMessage';
|
||||||
|
import { askAi } from '../utils/askAI';
|
||||||
|
import Splash from './Splash';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
|
export interface ChatType {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
messages: Array<{
|
||||||
|
id: string;
|
||||||
|
role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool';
|
||||||
|
content: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatView({ setView }: { setView: (view: View) => void }) {
|
||||||
|
const [chat, setChat] = useState<ChatType>(() => {
|
||||||
|
return {
|
||||||
|
id: 1,
|
||||||
|
title: 'Chat 1',
|
||||||
|
messages: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
|
||||||
|
const [hasMessages, setHasMessages] = useState(false);
|
||||||
|
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
|
||||||
|
const [showGame, setShowGame] = useState(false);
|
||||||
|
const scrollRef = useRef<ScrollAreaHandle>(null);
|
||||||
|
|
||||||
|
const { messages, append, stop, isLoading, error, setMessages } = useChat({
|
||||||
|
api: getApiUrl('/reply'),
|
||||||
|
initialMessages: chat?.messages || [],
|
||||||
|
onFinish: async (message, _) => {
|
||||||
|
window.electron.stopPowerSaveBlocker();
|
||||||
|
|
||||||
|
const fetchResponses = await askAi(message.content);
|
||||||
|
setMessageMetadata((prev) => ({ ...prev, [message.id]: fetchResponses }));
|
||||||
|
|
||||||
|
const timeSinceLastInteraction = Date.now() - lastInteractionTime;
|
||||||
|
window.electron.logInfo('last interaction:' + lastInteractionTime);
|
||||||
|
if (timeSinceLastInteraction > 60000) {
|
||||||
|
// 60000ms = 1 minute
|
||||||
|
window.electron.showNotification({
|
||||||
|
title: 'Goose finished the task.',
|
||||||
|
body: 'Click here to expand.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update chat messages when they change
|
||||||
|
useEffect(() => {
|
||||||
|
setChat({ ...chat, messages });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messages.length > 0) {
|
||||||
|
setHasMessages(true);
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Handle submit
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
window.electron.startPowerSaveBlocker();
|
||||||
|
const customEvent = e as CustomEvent;
|
||||||
|
const content = customEvent.detail?.value || '';
|
||||||
|
if (content.trim()) {
|
||||||
|
setLastInteractionTime(Date.now());
|
||||||
|
append({
|
||||||
|
role: 'user',
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
if (scrollRef.current?.scrollToBottom) {
|
||||||
|
scrollRef.current.scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.log('Error:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onStopGoose = () => {
|
||||||
|
stop();
|
||||||
|
setLastInteractionTime(Date.now());
|
||||||
|
window.electron.stopPowerSaveBlocker();
|
||||||
|
|
||||||
|
const lastMessage: Message = messages[messages.length - 1];
|
||||||
|
if (lastMessage.role === 'user' && lastMessage.toolInvocations === undefined) {
|
||||||
|
// Remove the last user message.
|
||||||
|
if (messages.length > 1) {
|
||||||
|
setMessages(messages.slice(0, -1));
|
||||||
|
} else {
|
||||||
|
setMessages([]);
|
||||||
|
}
|
||||||
|
} else if (lastMessage.role === 'assistant' && lastMessage.toolInvocations !== undefined) {
|
||||||
|
// Add messaging about interrupted ongoing tool invocations
|
||||||
|
const newLastMessage: Message = {
|
||||||
|
...lastMessage,
|
||||||
|
toolInvocations: lastMessage.toolInvocations.map((invocation) => {
|
||||||
|
if (invocation.state !== 'result') {
|
||||||
|
return {
|
||||||
|
...invocation,
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
audience: ['user'],
|
||||||
|
text: 'Interrupted.\n',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
audience: ['assistant'],
|
||||||
|
text: 'Interrupted by the user to make a correction.\n',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
state: 'result',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return invocation;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedMessages = [...messages.slice(0, -1), newLastMessage];
|
||||||
|
setMessages(updatedMessages);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-full h-screen items-center justify-center">
|
||||||
|
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle border-b border-borderSubtle">
|
||||||
|
<MoreMenu setView={setView} />
|
||||||
|
</div>
|
||||||
|
<Card className="flex flex-col flex-1 rounded-none h-[calc(100vh-95px)] w-full bg-bgApp mt-0 border-none relative">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<Splash append={append} />
|
||||||
|
) : (
|
||||||
|
<ScrollArea ref={scrollRef} className="flex-1 px-4" autoScroll>
|
||||||
|
{messages.map((message) => (
|
||||||
|
<div key={message.id} className="mt-[16px]">
|
||||||
|
{message.role === 'user' ? (
|
||||||
|
<UserMessage message={message} />
|
||||||
|
) : (
|
||||||
|
<GooseMessage
|
||||||
|
message={message}
|
||||||
|
messages={messages}
|
||||||
|
metadata={messageMetadata[message.id]}
|
||||||
|
append={append}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{error && (
|
||||||
|
<div className="flex flex-col items-center justify-center p-4">
|
||||||
|
<div className="text-red-700 dark:text-red-300 bg-red-400/50 p-3 rounded-lg mb-2">
|
||||||
|
{error.message || 'Honk! Goose experienced an error while responding'}
|
||||||
|
{error.status && <span className="ml-2">(Status: {error.status})</span>}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="px-3 py-2 mt-2 text-center whitespace-nowrap cursor-pointer text-textStandard border border-borderSubtle hover:bg-bgSubtle rounded-full inline-block transition-all duration-150"
|
||||||
|
onClick={async () => {
|
||||||
|
const lastUserMessage = messages.reduceRight(
|
||||||
|
(found, m) => found || (m.role === 'user' ? m : null),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
if (lastUserMessage) {
|
||||||
|
append({
|
||||||
|
role: 'user',
|
||||||
|
content: lastUserMessage.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retry Last Message
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="block h-16" />
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
{isLoading && <LoadingGoose />}
|
||||||
|
<Input
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
disabled={isLoading}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onStop={onStopGoose}
|
||||||
|
/>
|
||||||
|
<BottomMenu hasMessages={hasMessages} setView={setView} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
ui/desktop/src/components/ErrorBoundary.tsx
Normal file
40
ui/desktop/src/components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// Capture unhandled promise rejections
|
||||||
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
window.electron.logInfo(`[UNHANDLED REJECTION] ${event.reason}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Capture global errors
|
||||||
|
window.addEventListener('error', (event) => {
|
||||||
|
window.electron.logInfo(
|
||||||
|
`[GLOBAL ERROR] ${event.message} at ${event.filename}:${event.lineno}:${event.colno}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export class ErrorBoundary extends React.Component<
|
||||||
|
{ children: React.ReactNode },
|
||||||
|
{ hasError: boolean }
|
||||||
|
> {
|
||||||
|
constructor(props: { children: React.ReactNode }) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(_: Error) {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
|
// Send error to main process
|
||||||
|
window.electron.logInfo(`[ERROR] ${error.toString()}\n${errorInfo.componentStack}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return <h1>Something went wrong.</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Popover, PopoverContent, PopoverTrigger, PopoverPortal } from '@radix-ui/react-popover';
|
import { Popover, PopoverContent, PopoverTrigger, PopoverPortal } from '@radix-ui/react-popover';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { More } from './icons';
|
import { More } from './icons';
|
||||||
import type { View } from '../../ChatWindow';
|
import type { View } from '../ChatWindow';
|
||||||
|
|
||||||
interface VersionInfo {
|
interface VersionInfo {
|
||||||
current_version: string;
|
current_version: string;
|
||||||
available_versions: string[];
|
available_versions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept setView as a prop from the parent (e.g. ChatContent)
|
// Accept setView as a prop from the parent (e.g. Chat)
|
||||||
export default function MoreMenu({ setView }: { setView: (view: View) => void }) {
|
export default function MoreMenu({ setView }: { setView: (view: View) => void }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [versions, setVersions] = useState<VersionInfo | null>(null);
|
const [versions, setVersions] = useState<VersionInfo | null>(null);
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ import {
|
|||||||
supported_providers,
|
supported_providers,
|
||||||
required_keys,
|
required_keys,
|
||||||
provider_aliases,
|
provider_aliases,
|
||||||
} from '../settings/models/hardcoded_stuff';
|
} from './settings/models/hardcoded_stuff';
|
||||||
import { useActiveKeys } from '../settings/api_keys/ActiveKeysContext';
|
import { useActiveKeys } from './settings/api_keys/ActiveKeysContext';
|
||||||
import { ProviderSetupModal } from '../settings/ProviderSetupModal';
|
import { ProviderSetupModal } from './settings/ProviderSetupModal';
|
||||||
import { useModel } from '../settings/models/ModelContext';
|
import { useModel } from './settings/models/ModelContext';
|
||||||
import { useRecentModels } from '../settings/models/RecentModels';
|
import { useRecentModels } from './settings/models/RecentModels';
|
||||||
import { createSelectedModel } from '../settings/models/utils';
|
import { createSelectedModel } from './settings/models/utils';
|
||||||
import { getDefaultModel } from '../settings/models/hardcoded_stuff';
|
import { getDefaultModel } from './settings/models/hardcoded_stuff';
|
||||||
import { initializeSystem } from '../../utils/providerUtils';
|
import { initializeSystem } from '../utils/providerUtils';
|
||||||
import { getApiUrl, getSecretKey } from '../../config';
|
import { getApiUrl, getSecretKey } from '../config';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { getActiveProviders, isSecretKey } from '../settings/api_keys/utils';
|
import { getActiveProviders, isSecretKey } from './settings/api_keys/utils';
|
||||||
import { BaseProviderGrid, getProviderDescription } from '../settings/providers/BaseProviderGrid';
|
import { BaseProviderGrid, getProviderDescription } from './settings/providers/BaseProviderGrid';
|
||||||
|
|
||||||
interface ProviderGridProps {
|
interface ProviderGridProps {
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card } from './ui/card';
|
|
||||||
import { Bird } from './ui/icons';
|
|
||||||
|
|
||||||
interface WelcomeScreenProps {
|
|
||||||
className?: string;
|
|
||||||
onDismiss: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WelcomeScreen({ className, onDismiss }: WelcomeScreenProps) {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={`flex flex-col items-center justify-center p-8 space-y-6 bg-bgApp w-full h-full ${className}`}
|
|
||||||
>
|
|
||||||
<div className="w-16 h-16">
|
|
||||||
<Bird />
|
|
||||||
</div>
|
|
||||||
<div className="text-center space-y-4">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-800 dark:text-white/70">
|
|
||||||
Welcome to Goose 1.0 <b>beta</b>! 🎉
|
|
||||||
</h2>
|
|
||||||
<div className="whitespace-pre-wrap text-gray-600 dark:text-white/50">
|
|
||||||
Goose is your AI-powered agent.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<b>
|
|
||||||
{' '}
|
|
||||||
Warning: During the beta, your chats are not saved - closing the window <br />
|
|
||||||
or closing the app will lose your history. <br />
|
|
||||||
</b>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Try ⌘+N for a new window, or ⌘+O to work on a specific directory.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={onDismiss}
|
|
||||||
className="mt-6 px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
|
|
||||||
>
|
|
||||||
Get Started
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ProviderGrid } from './ProviderGrid';
|
import { ProviderGrid } from './ProviderGrid';
|
||||||
import { ScrollArea } from '../ui/scroll-area';
|
import { ScrollArea } from './ui/scroll-area';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from './ui/button';
|
||||||
import WelcomeGooseLogo from '../WelcomeGooseLogo';
|
import WelcomeGooseLogo from './WelcomeGooseLogo';
|
||||||
|
|
||||||
// Extending React CSSProperties to include custom webkit property
|
// Extending React CSSProperties to include custom webkit property
|
||||||
declare module 'react' {
|
declare module 'react' {
|
||||||
@@ -15,7 +15,7 @@ interface WelcomeScreenProps {
|
|||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WelcomeScreen({ onSubmit }: WelcomeScreenProps) {
|
export default function WelcomeScreen({ onSubmit }: WelcomeScreenProps) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full select-none bg-white dark:bg-black">
|
<div className="h-screen w-full select-none bg-white dark:bg-black">
|
||||||
{/* Draggable title bar region */}
|
{/* Draggable title bar region */}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Bird } from '../components/ui/icons';
|
|
||||||
|
|
||||||
export enum Working {
|
|
||||||
Idle = 'Idle',
|
|
||||||
Working = 'Working',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WingToWingProps {
|
|
||||||
onExpand: () => void;
|
|
||||||
progressMessage: string;
|
|
||||||
working: Working;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WingToWing: React.FC<WingToWingProps> = ({ onExpand, progressMessage, working }) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={onExpand}
|
|
||||||
className="flex items-center w-full h-28 bg-gradient-to-r from-gray-100 via-gray-200 to-gray-300 shadow-md rounded-lg p-4 cursor-pointer hover:shadow-lg transition-all duration-200"
|
|
||||||
>
|
|
||||||
{working === Working.Working && (
|
|
||||||
<div className="w-10 h-10 mr-4 flex-shrink-0">
|
|
||||||
<Bird />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Status Text */}
|
|
||||||
<div className="flex flex-col text-left">
|
|
||||||
<span className="text-sm text-gray-600 font-medium">{progressMessage}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WingToWing;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export const ChatLayout = ({ children, mode }) => (
|
|
||||||
<div className="relative w-screen h-screen overflow-hidden bg-bgApp flex flex-col">
|
|
||||||
<div className="titlebar-drag-region" />
|
|
||||||
<div style={{ display: mode === 'expanded' ? 'block' : 'none' }}>{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
@@ -14,7 +14,7 @@ import { ConfigureBuiltInExtensionModal } from './extensions/ConfigureBuiltInExt
|
|||||||
import BackButton from '../ui/BackButton';
|
import BackButton from '../ui/BackButton';
|
||||||
import { RecentModelsRadio } from './models/RecentModels';
|
import { RecentModelsRadio } from './models/RecentModels';
|
||||||
import { ExtensionItem } from './extensions/ExtensionItem';
|
import { ExtensionItem } from './extensions/ExtensionItem';
|
||||||
import type { View } from '../../ChatWindow';
|
import type { View } from '../../App';
|
||||||
|
|
||||||
const EXTENSIONS_DESCRIPTION =
|
const EXTENSIONS_DESCRIPTION =
|
||||||
'The Model Context Protocol (MCP) is a system that allows AI models to securely connect with local or remote resources using standard server setups. It works like a client-server setup and expands AI capabilities using three main components: Prompts, Resources, and Tools.';
|
'The Model Context Protocol (MCP) is a system that allows AI models to securely connect with local or remote resources using standard server setups. It works like a client-server setup and expands AI capabilities using three main components: Prompts, Resources, and Tools.';
|
||||||
@@ -46,10 +46,7 @@ const DEFAULT_SETTINGS: SettingsType = {
|
|||||||
extensions: BUILT_IN_EXTENSIONS,
|
extensions: BUILT_IN_EXTENSIONS,
|
||||||
};
|
};
|
||||||
|
|
||||||
// We'll accept two props:
|
export default function SettingsView({
|
||||||
// onClose: to go back to chat
|
|
||||||
// setView: to switch to moreModels, configureProviders, etc.
|
|
||||||
export default function Settings({
|
|
||||||
onClose,
|
onClose,
|
||||||
setView,
|
setView,
|
||||||
}: {
|
}: {
|
||||||
@@ -3,20 +3,17 @@ import { RecentModels } from './RecentModels';
|
|||||||
import { ProviderButtons } from './ProviderButtons';
|
import { ProviderButtons } from './ProviderButtons';
|
||||||
import BackButton from '../../ui/BackButton';
|
import BackButton from '../../ui/BackButton';
|
||||||
import { SearchBar } from './Search';
|
import { SearchBar } from './Search';
|
||||||
import { useModel } from './ModelContext';
|
|
||||||
import { AddModelInline } from './AddModelInline';
|
import { AddModelInline } from './AddModelInline';
|
||||||
import { ScrollArea } from '../../ui/scroll-area';
|
import { ScrollArea } from '../../ui/scroll-area';
|
||||||
import type { View } from '../../../ChatWindow';
|
import type { View } from '../../../App';
|
||||||
|
|
||||||
export default function MoreModelsPage({
|
export default function MoreModelsView({
|
||||||
onClose,
|
onClose,
|
||||||
setView,
|
setView,
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
setView: (view: View) => void;
|
setView: (view: View) => void;
|
||||||
}) {
|
}) {
|
||||||
const { currentModel } = useModel();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full">
|
<div className="h-screen w-full">
|
||||||
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||||
@@ -2,15 +2,8 @@ import React from 'react';
|
|||||||
import { ScrollArea } from '../../ui/scroll-area';
|
import { ScrollArea } from '../../ui/scroll-area';
|
||||||
import BackButton from '../../ui/BackButton';
|
import BackButton from '../../ui/BackButton';
|
||||||
import { ConfigureProvidersGrid } from './ConfigureProvidersGrid';
|
import { ConfigureProvidersGrid } from './ConfigureProvidersGrid';
|
||||||
import type { View } from '../../../ChatWindow';
|
|
||||||
|
|
||||||
export default function ConfigureProviders({
|
export default function ConfigureProvidersView({ onClose }: { onClose: () => void }) {
|
||||||
onClose,
|
|
||||||
setView,
|
|
||||||
}: {
|
|
||||||
onClose: () => void;
|
|
||||||
setView?: (view: View) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full">
|
<div className="h-screen w-full">
|
||||||
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getApiUrl, getSecretKey } from './config';
|
import { getApiUrl, getSecretKey } from './config';
|
||||||
|
import { type View } from './App';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
// ExtensionConfig type matching the Rust version
|
// ExtensionConfig type matching the Rust version
|
||||||
@@ -257,7 +258,7 @@ function handleError(message: string, shouldThrow = false): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addExtensionFromDeepLink(url: string, navigate: any) {
|
export async function addExtensionFromDeepLink(url: string, setView: (view: View) => void) {
|
||||||
if (!url.startsWith('goose://extension')) {
|
if (!url.startsWith('goose://extension')) {
|
||||||
handleError(
|
handleError(
|
||||||
'Failed to install extension: Invalid URL: URL must use the goose://extension scheme'
|
'Failed to install extension: Invalid URL: URL must use the goose://extension scheme'
|
||||||
@@ -343,7 +344,9 @@ export async function addExtensionFromDeepLink(url: string, navigate: any) {
|
|||||||
// Check if extension requires env vars and go to settings if so
|
// Check if extension requires env vars and go to settings if so
|
||||||
if (envVarsRequired(config)) {
|
if (envVarsRequired(config)) {
|
||||||
console.log('Environment variables required, redirecting to settings');
|
console.log('Environment variables required, redirecting to settings');
|
||||||
navigate(`/settings?extensionId=${config.id}&showEnvVars=true`);
|
setView('settings');
|
||||||
|
// TODO - add code which can auto-open the modal on the settings view
|
||||||
|
// navigate(`/settings?extensionId=${config.id}&showEnvVars=true`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,47 +181,6 @@ let appConfig = {
|
|||||||
secretKey: generateSecretKey(),
|
secretKey: generateSecretKey(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const createLauncher = () => {
|
|
||||||
const launcherWindow = new BrowserWindow({
|
|
||||||
width: 600,
|
|
||||||
height: 60,
|
|
||||||
frame: process.platform === 'darwin' ? false : true,
|
|
||||||
transparent: false,
|
|
||||||
webPreferences: {
|
|
||||||
preload: path.join(__dirname, 'preload.ts'),
|
|
||||||
additionalArguments: [JSON.stringify(appConfig)],
|
|
||||||
partition: 'persist:goose',
|
|
||||||
},
|
|
||||||
skipTaskbar: true,
|
|
||||||
alwaysOnTop: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Center on screen
|
|
||||||
const primaryDisplay = electron.screen.getPrimaryDisplay();
|
|
||||||
const { width, height } = primaryDisplay.workAreaSize;
|
|
||||||
const windowBounds = launcherWindow.getBounds();
|
|
||||||
|
|
||||||
launcherWindow.setPosition(
|
|
||||||
Math.round(width / 2 - windowBounds.width / 2),
|
|
||||||
Math.round(height / 3 - windowBounds.height / 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load launcher window content
|
|
||||||
const launcherParams = '?window=launcher#/launcher';
|
|
||||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
|
||||||
launcherWindow.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}${launcherParams}`);
|
|
||||||
} else {
|
|
||||||
launcherWindow.loadFile(
|
|
||||||
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html${launcherParams}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy window when it loses focus
|
|
||||||
launcherWindow.on('blur', () => {
|
|
||||||
launcherWindow.destroy();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Track windows by ID
|
// Track windows by ID
|
||||||
let windowCounter = 0;
|
let windowCounter = 0;
|
||||||
const windowMap = new Map<number, BrowserWindow>();
|
const windowMap = new Map<number, BrowserWindow>();
|
||||||
@@ -487,9 +446,6 @@ app.whenReady().then(async () => {
|
|||||||
let openDir = dirPath || (recentDirs.length > 0 ? recentDirs[0] : null);
|
let openDir = dirPath || (recentDirs.length > 0 ? recentDirs[0] : null);
|
||||||
createChat(app, undefined, openDir);
|
createChat(app, undefined, openDir);
|
||||||
|
|
||||||
// Show launcher input on key combo
|
|
||||||
globalShortcut.register('Control+Alt+Command+G', createLauncher);
|
|
||||||
|
|
||||||
// Get the existing menu
|
// Get the existing menu
|
||||||
const menu = Menu.getApplicationMenu();
|
const menu = Menu.getApplicationMenu();
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron')
|
|
||||||
|
|
||||||
const config = JSON.parse(process.argv.find((arg) => arg.startsWith('{')) || '{}');
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('appConfig', {
|
|
||||||
get: (key) => config[key],
|
|
||||||
getAll: () => config,
|
|
||||||
});
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron', {
|
|
||||||
getConfig: () => config,
|
|
||||||
hideWindow: () => ipcRenderer.send('hide-window'),
|
|
||||||
directoryChooser: (replace) => ipcRenderer.send('directory-chooser', replace),
|
|
||||||
createChatWindow: (query, dir, version) => ipcRenderer.send('create-chat-window', query, dir, version),
|
|
||||||
logInfo: (txt) => ipcRenderer.send('logInfo', txt),
|
|
||||||
showNotification: (data) => ipcRenderer.send('notify', data),
|
|
||||||
createWingToWingWindow: (query) => ipcRenderer.send('create-wing-to-wing-window', query),
|
|
||||||
openInChrome: (url) => ipcRenderer.send('open-in-chrome', url),
|
|
||||||
fetchMetadata: (url) => ipcRenderer.invoke('fetch-metadata', url),
|
|
||||||
reloadApp: () => ipcRenderer.send('reload-app'),
|
|
||||||
checkForOllama: () => ipcRenderer.invoke('check-ollama'),
|
|
||||||
selectFileOrDirectory: () => ipcRenderer.invoke('select-file-or-directory'),
|
|
||||||
startPowerSaveBlocker: () => ipcRenderer.invoke('start-power-save-blocker'),
|
|
||||||
stopPowerSaveBlocker: () => ipcRenderer.invoke('stop-power-save-blocker'),
|
|
||||||
getBinaryPath: (binaryName) => ipcRenderer.invoke('get-binary-path', binaryName),
|
|
||||||
on: (channel, callback) => ipcRenderer.on(channel, callback),
|
|
||||||
off: (channel, callback) => ipcRenderer.off(channel, callback),
|
|
||||||
send: (key) => ipcRenderer.send(key)
|
|
||||||
});
|
|
||||||
@@ -10,7 +10,6 @@ type ElectronAPI = {
|
|||||||
createChatWindow: (query?: string, dir?: string, version?: string) => void;
|
createChatWindow: (query?: string, dir?: string, version?: string) => void;
|
||||||
logInfo: (txt: string) => void;
|
logInfo: (txt: string) => void;
|
||||||
showNotification: (data: any) => void;
|
showNotification: (data: any) => void;
|
||||||
createWingToWingWindow: (query: string) => void;
|
|
||||||
openInChrome: (url: string) => void;
|
openInChrome: (url: string) => void;
|
||||||
fetchMetadata: (url: string) => Promise<any>;
|
fetchMetadata: (url: string) => Promise<any>;
|
||||||
reloadApp: () => void;
|
reloadApp: () => void;
|
||||||
@@ -42,7 +41,6 @@ const electronAPI: ElectronAPI = {
|
|||||||
ipcRenderer.send('create-chat-window', query, dir, version),
|
ipcRenderer.send('create-chat-window', query, dir, version),
|
||||||
logInfo: (txt: string) => ipcRenderer.send('logInfo', txt),
|
logInfo: (txt: string) => ipcRenderer.send('logInfo', txt),
|
||||||
showNotification: (data: any) => ipcRenderer.send('notify', data),
|
showNotification: (data: any) => ipcRenderer.send('notify', data),
|
||||||
createWingToWingWindow: (query: string) => ipcRenderer.send('create-wing-to-wing-window', query),
|
|
||||||
openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url),
|
openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url),
|
||||||
fetchMetadata: (url: string) => ipcRenderer.invoke('fetch-metadata', url),
|
fetchMetadata: (url: string) => ipcRenderer.invoke('fetch-metadata', url),
|
||||||
reloadApp: () => ipcRenderer.send('reload-app'),
|
reloadApp: () => ipcRenderer.send('reload-app'),
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file will automatically be loaded by vite and run in the "renderer" context.
|
|
||||||
* To learn more about the differences between the "main" and the "renderer" context in
|
|
||||||
* Electron, visit:
|
|
||||||
*
|
|
||||||
* https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes
|
|
||||||
*
|
|
||||||
* By default, Node.js integration in this file is disabled. When enabling Node.js integration
|
|
||||||
* in a renderer process, please be aware of potential security implications. You can read
|
|
||||||
* more about security risks here:
|
|
||||||
*
|
|
||||||
* https://electronjs.org/docs/tutorial/security
|
|
||||||
*
|
|
||||||
* To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration`
|
|
||||||
* flag:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* // Create the browser window.
|
|
||||||
* mainWindow = new BrowserWindow({
|
|
||||||
* width: 800,
|
|
||||||
* height: 600,
|
|
||||||
* webPreferences: {
|
|
||||||
* nodeIntegration: true
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
|
|
||||||
import "/styles/main.css";
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'👋 This message is being logged by "renderer.js", included via Vite'
|
|
||||||
);
|
|
||||||
@@ -1,77 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import { ModelProvider } from './components/settings/models/ModelContext';
|
||||||
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||||
|
import { ActiveKeysProvider } from './components/settings/api_keys/ActiveKeysContext';
|
||||||
|
import { patchConsoleLogging } from './utils';
|
||||||
|
|
||||||
// Error Boundary Component
|
patchConsoleLogging();
|
||||||
class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
|
|
||||||
constructor(props: { children: React.ReactNode }) {
|
|
||||||
super(props);
|
|
||||||
this.state = { hasError: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromError(_: Error) {
|
|
||||||
return { hasError: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
||||||
// Send error to main process
|
|
||||||
window.electron.logInfo(`[ERROR] ${error.toString()}\n${errorInfo.componentStack}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
return <h1>Something went wrong.</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up console interceptors
|
|
||||||
const originalConsole = {
|
|
||||||
log: console.log,
|
|
||||||
error: console.error,
|
|
||||||
warn: console.warn,
|
|
||||||
info: console.info,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Intercept console methods
|
|
||||||
console.log = (...args) => {
|
|
||||||
window.electron.logInfo(`[LOG] ${args.join(' ')}`);
|
|
||||||
originalConsole.log(...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
console.error = (...args) => {
|
|
||||||
window.electron.logInfo(`[ERROR] ${args.join(' ')}`);
|
|
||||||
originalConsole.error(...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
console.warn = (...args) => {
|
|
||||||
window.electron.logInfo(`[WARN] ${args.join(' ')}`);
|
|
||||||
originalConsole.warn(...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
console.info = (...args) => {
|
|
||||||
window.electron.logInfo(`[INFO] ${args.join(' ')}`);
|
|
||||||
originalConsole.info(...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Capture unhandled promise rejections
|
|
||||||
window.addEventListener('unhandledrejection', (event) => {
|
|
||||||
window.electron.logInfo(`[UNHANDLED REJECTION] ${event.reason}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Capture global errors
|
|
||||||
window.addEventListener('error', (event) => {
|
|
||||||
window.electron.logInfo(
|
|
||||||
`[GLOBAL ERROR] ${event.message} at ${event.filename}:${event.lineno}:${event.colno}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<ModelProvider>
|
||||||
|
<ActiveKeysProvider>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<App />
|
<App />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
</ActiveKeysProvider>
|
||||||
|
</ModelProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,3 +11,33 @@ export function snakeToTitleCase(snake: string): string {
|
|||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||||
.join(' ');
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function patchConsoleLogging() {
|
||||||
|
// Intercept console methods
|
||||||
|
const originalConsole = {
|
||||||
|
log: console.log,
|
||||||
|
error: console.error,
|
||||||
|
warn: console.warn,
|
||||||
|
info: console.info,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log = (...args: any[]) => {
|
||||||
|
window.electron.logInfo(`[LOG] ${args.join(' ')}`);
|
||||||
|
originalConsole.log(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.error = (...args: any[]) => {
|
||||||
|
window.electron.logInfo(`[ERROR] ${args.join(' ')}`);
|
||||||
|
originalConsole.error(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.warn = (...args: any[]) => {
|
||||||
|
window.electron.logInfo(`[WARN] ${args.join(' ')}`);
|
||||||
|
originalConsole.warn(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.info = (...args: any[]) => {
|
||||||
|
window.electron.logInfo(`[INFO] ${args.join(' ')}`);
|
||||||
|
originalConsole.info(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"text": "Hello! ping www.apple.com 10 times."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# run cargo run -p goose-server: https://github.com/block/goose/pull/237
|
|
||||||
|
|
||||||
curl -N http://localhost:3000/reply \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Accept: text/event-stream" \
|
|
||||||
-H "x-protocol: data" \
|
|
||||||
-d @test.json
|
|
||||||
Reference in New Issue
Block a user