refactor: Component hierarchy and code cleanup (#1319)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 React, { useEffect, useState } from 'react';
|
||||
import { More } from './icons';
|
||||
import type { View } from '../../ChatWindow';
|
||||
import type { View } from '../ChatWindow';
|
||||
|
||||
interface VersionInfo {
|
||||
current_version: 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 }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [versions, setVersions] = useState<VersionInfo | null>(null);
|
||||
|
||||
+11
-11
@@ -3,18 +3,18 @@ import {
|
||||
supported_providers,
|
||||
required_keys,
|
||||
provider_aliases,
|
||||
} from '../settings/models/hardcoded_stuff';
|
||||
import { useActiveKeys } from '../settings/api_keys/ActiveKeysContext';
|
||||
import { ProviderSetupModal } from '../settings/ProviderSetupModal';
|
||||
import { useModel } from '../settings/models/ModelContext';
|
||||
import { useRecentModels } from '../settings/models/RecentModels';
|
||||
import { createSelectedModel } from '../settings/models/utils';
|
||||
import { getDefaultModel } from '../settings/models/hardcoded_stuff';
|
||||
import { initializeSystem } from '../../utils/providerUtils';
|
||||
import { getApiUrl, getSecretKey } from '../../config';
|
||||
} from './settings/models/hardcoded_stuff';
|
||||
import { useActiveKeys } from './settings/api_keys/ActiveKeysContext';
|
||||
import { ProviderSetupModal } from './settings/ProviderSetupModal';
|
||||
import { useModel } from './settings/models/ModelContext';
|
||||
import { useRecentModels } from './settings/models/RecentModels';
|
||||
import { createSelectedModel } from './settings/models/utils';
|
||||
import { getDefaultModel } from './settings/models/hardcoded_stuff';
|
||||
import { initializeSystem } from '../utils/providerUtils';
|
||||
import { getApiUrl, getSecretKey } from '../config';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getActiveProviders, isSecretKey } from '../settings/api_keys/utils';
|
||||
import { BaseProviderGrid, getProviderDescription } from '../settings/providers/BaseProviderGrid';
|
||||
import { getActiveProviders, isSecretKey } from './settings/api_keys/utils';
|
||||
import { BaseProviderGrid, getProviderDescription } from './settings/providers/BaseProviderGrid';
|
||||
|
||||
interface ProviderGridProps {
|
||||
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>
|
||||
);
|
||||
}
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { ProviderGrid } from './ProviderGrid';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { Button } from '../ui/button';
|
||||
import WelcomeGooseLogo from '../WelcomeGooseLogo';
|
||||
import { ScrollArea } from './ui/scroll-area';
|
||||
import { Button } from './ui/button';
|
||||
import WelcomeGooseLogo from './WelcomeGooseLogo';
|
||||
|
||||
// Extending React CSSProperties to include custom webkit property
|
||||
declare module 'react' {
|
||||
@@ -15,7 +15,7 @@ interface WelcomeScreenProps {
|
||||
onSubmit?: () => void;
|
||||
}
|
||||
|
||||
export function WelcomeScreen({ onSubmit }: WelcomeScreenProps) {
|
||||
export default function WelcomeScreen({ onSubmit }: WelcomeScreenProps) {
|
||||
return (
|
||||
<div className="h-screen w-full select-none bg-white dark:bg-black">
|
||||
{/* 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>
|
||||
);
|
||||
+2
-5
@@ -14,7 +14,7 @@ import { ConfigureBuiltInExtensionModal } from './extensions/ConfigureBuiltInExt
|
||||
import BackButton from '../ui/BackButton';
|
||||
import { RecentModelsRadio } from './models/RecentModels';
|
||||
import { ExtensionItem } from './extensions/ExtensionItem';
|
||||
import type { View } from '../../ChatWindow';
|
||||
import type { View } from '../../App';
|
||||
|
||||
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.';
|
||||
@@ -46,10 +46,7 @@ const DEFAULT_SETTINGS: SettingsType = {
|
||||
extensions: BUILT_IN_EXTENSIONS,
|
||||
};
|
||||
|
||||
// We'll accept two props:
|
||||
// onClose: to go back to chat
|
||||
// setView: to switch to moreModels, configureProviders, etc.
|
||||
export default function Settings({
|
||||
export default function SettingsView({
|
||||
onClose,
|
||||
setView,
|
||||
}: {
|
||||
+2
-5
@@ -3,20 +3,17 @@ import { RecentModels } from './RecentModels';
|
||||
import { ProviderButtons } from './ProviderButtons';
|
||||
import BackButton from '../../ui/BackButton';
|
||||
import { SearchBar } from './Search';
|
||||
import { useModel } from './ModelContext';
|
||||
import { AddModelInline } from './AddModelInline';
|
||||
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,
|
||||
setView,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
setView: (view: View) => void;
|
||||
}) {
|
||||
const { currentModel } = useModel();
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full">
|
||||
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||
+1
-8
@@ -2,15 +2,8 @@ import React from 'react';
|
||||
import { ScrollArea } from '../../ui/scroll-area';
|
||||
import BackButton from '../../ui/BackButton';
|
||||
import { ConfigureProvidersGrid } from './ConfigureProvidersGrid';
|
||||
import type { View } from '../../../ChatWindow';
|
||||
|
||||
export default function ConfigureProviders({
|
||||
onClose,
|
||||
setView,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
setView?: (view: View) => void;
|
||||
}) {
|
||||
export default function ConfigureProvidersView({ onClose }: { onClose: () => void }) {
|
||||
return (
|
||||
<div className="h-screen w-full">
|
||||
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||
Reference in New Issue
Block a user