Fix ESLint warnings and and enable max warnings 0 to fail builds (#2101)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useModel } from './settings/models/ModelContext';
|
||||
import { useRecentModels } from './settings/models/RecentModels'; // Hook for recent models
|
||||
import { Sliders } from 'lucide-react';
|
||||
import { ModelRadioList } from './settings/models/ModelRadioList';
|
||||
import { Document, ChevronUp, ChevronDown } from './icons';
|
||||
@@ -19,7 +18,6 @@ export default function BottomMenu({
|
||||
}) {
|
||||
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
|
||||
const { currentModel } = useModel();
|
||||
const { recentModels } = useRecentModels(); // Get recent models
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Add effect to handle clicks outside
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { getApiUrl, getSecretKey } from '../config';
|
||||
import { ChevronDown, ChevronUp } from './icons';
|
||||
import {
|
||||
@@ -16,35 +16,35 @@ export const BottomMenuModeSelection = () => {
|
||||
const gooseModeDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const { read, upsert } = useConfig();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCurrentMode = async () => {
|
||||
try {
|
||||
if (!settingsV2Enabled) {
|
||||
const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': getSecretKey(),
|
||||
},
|
||||
});
|
||||
const fetchCurrentMode = useCallback(async () => {
|
||||
try {
|
||||
if (!settingsV2Enabled) {
|
||||
const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': getSecretKey(),
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const { value } = await response.json();
|
||||
if (value) {
|
||||
setGooseMode(value);
|
||||
}
|
||||
if (response.ok) {
|
||||
const { value } = await response.json();
|
||||
if (value) {
|
||||
setGooseMode(value);
|
||||
}
|
||||
} else {
|
||||
const mode = (await read('GOOSE_MODE', false)) as string;
|
||||
setGooseMode(mode);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching current mode:', error);
|
||||
} else {
|
||||
const mode = (await read('GOOSE_MODE', false)) as string;
|
||||
setGooseMode(mode);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching current mode:', error);
|
||||
}
|
||||
}, [read]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrentMode();
|
||||
}, []);
|
||||
}, [fetchCurrentMode]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEsc = (event: KeyboardEvent) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import BottomMenu from './BottomMenu';
|
||||
import FlappyGoose from './FlappyGoose';
|
||||
import GooseMessage from './GooseMessage';
|
||||
import Input from './Input';
|
||||
import { type View } from '../App';
|
||||
import { type View, ViewOptions } from '../App';
|
||||
import LoadingGoose from './LoadingGoose';
|
||||
import MoreMenuLayout from './more_menu/MoreMenuLayout';
|
||||
import { Card } from './ui/card';
|
||||
@@ -22,11 +22,9 @@ import {
|
||||
ToolCall,
|
||||
ToolCallResult,
|
||||
ToolRequestMessageContent,
|
||||
ToolResponse,
|
||||
ToolResponseMessageContent,
|
||||
ToolConfirmationRequestMessageContent,
|
||||
getTextContent,
|
||||
createAssistantMessage,
|
||||
} from '../types/message';
|
||||
|
||||
export interface ChatType {
|
||||
@@ -38,6 +36,26 @@ export interface ChatType {
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
interface GeneratedBotConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
activities: string[];
|
||||
}
|
||||
|
||||
// Helper function to determine if a message is a user message
|
||||
const isUserMessage = (message: Message): boolean => {
|
||||
if (message.role === 'assistant') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.content.every((c) => c.type === 'toolConfirmationRequest')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default function ChatView({
|
||||
chat,
|
||||
setChat,
|
||||
@@ -46,16 +64,17 @@ export default function ChatView({
|
||||
}: {
|
||||
chat: ChatType;
|
||||
setChat: (chat: ChatType) => void;
|
||||
setView: (view: View, viewOptions?: Record<any, any>) => void;
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
|
||||
}) {
|
||||
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
|
||||
// Disabled askAi calls to save costs
|
||||
// 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 [waitingForAgentResponse, setWaitingForAgentResponse] = useState(false);
|
||||
const [showShareableBotModal, setshowShareableBotModal] = useState(false);
|
||||
const [generatedBotConfig, setGeneratedBotConfig] = useState<any>(null);
|
||||
const [generatedBotConfig, setGeneratedBotConfig] = useState<GeneratedBotConfig | null>(null);
|
||||
const scrollRef = useRef<ScrollAreaHandle>(null);
|
||||
|
||||
// Get botConfig directly from appConfig
|
||||
@@ -76,7 +95,7 @@ export default function ChatView({
|
||||
api: getApiUrl('/reply'),
|
||||
initialMessages: chat.messages,
|
||||
body: { session_id: chat.id, session_working_dir: window.appConfig.get('GOOSE_WORKING_DIR') },
|
||||
onFinish: async (message, _reason) => {
|
||||
onFinish: async (_message, _reason) => {
|
||||
window.electron.stopPowerSaveBlocker();
|
||||
|
||||
// Disabled askAi calls to save costs
|
||||
@@ -94,7 +113,7 @@ export default function ChatView({
|
||||
});
|
||||
}
|
||||
},
|
||||
onToolCall: (toolCall) => {
|
||||
onToolCall: (toolCall: string) => {
|
||||
// Handle tool calls if needed
|
||||
console.log('Tool call received:', toolCall);
|
||||
// Implement tool call handling logic here
|
||||
@@ -213,7 +232,7 @@ export default function ChatView({
|
||||
const updatedChat = { ...prevChat, messages };
|
||||
return updatedChat;
|
||||
});
|
||||
}, [messages]);
|
||||
}, [messages, setChat]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
@@ -304,8 +323,6 @@ export default function ChatView({
|
||||
content: [],
|
||||
};
|
||||
|
||||
// get the last tool's name or just "tool"
|
||||
const lastToolName = toolRequests.at(-1)?.[1].value?.name ?? 'tool';
|
||||
const notification = 'Interrupted by the user to make a correction';
|
||||
|
||||
// generate a response saying it was interrupted for each tool request
|
||||
@@ -349,17 +366,6 @@ export default function ChatView({
|
||||
return true;
|
||||
});
|
||||
|
||||
const isUserMessage = (message: Message) => {
|
||||
if (message.role === 'assistant') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.content.every((c) => c.type === 'toolConfirmationRequest')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const commandHistory = useMemo(() => {
|
||||
return filteredMessages
|
||||
.reduce<string[]>((history, message) => {
|
||||
@@ -372,7 +378,7 @@ export default function ChatView({
|
||||
return history;
|
||||
}, [])
|
||||
.reverse();
|
||||
}, [filteredMessages, isUserMessage]);
|
||||
}, [filteredMessages]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-screen items-center justify-center">
|
||||
@@ -398,7 +404,7 @@ export default function ChatView({
|
||||
messageHistoryIndex={chat?.messageHistoryIndex}
|
||||
message={message}
|
||||
messages={messages}
|
||||
metadata={messageMetadata[message.id || '']}
|
||||
// metadata={messageMetadata[message.id || '']}
|
||||
append={(text) => append(createUserMessage(text))}
|
||||
appendMessage={(newMessage) => {
|
||||
const updatedMessages = [...messages, newMessage];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
|
||||
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
readAllConfig,
|
||||
readConfig,
|
||||
@@ -70,6 +70,115 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
const [providersList, setProvidersList] = useState<ProviderDetails[]>([]);
|
||||
const [extensionsList, setExtensionsList] = useState<FixedExtensionEntry[]>([]);
|
||||
|
||||
const reloadConfig = useCallback(async () => {
|
||||
const response = await readAllConfig();
|
||||
setConfig(response.data.config || {});
|
||||
}, []);
|
||||
|
||||
const upsert = useCallback(
|
||||
async (key: string, value: unknown, isSecret: boolean = false) => {
|
||||
const query: UpsertConfigQuery = {
|
||||
key: key,
|
||||
value: value,
|
||||
is_secret: isSecret,
|
||||
};
|
||||
await upsertConfig({
|
||||
body: query,
|
||||
});
|
||||
await reloadConfig();
|
||||
},
|
||||
[reloadConfig]
|
||||
);
|
||||
|
||||
const read = useCallback(async (key: string, is_secret: boolean = false) => {
|
||||
const query: ConfigKeyQuery = { key: key, is_secret: is_secret };
|
||||
const response = await readConfig({
|
||||
body: query,
|
||||
});
|
||||
return response.data;
|
||||
}, []);
|
||||
|
||||
const remove = useCallback(
|
||||
async (key: string, is_secret: boolean) => {
|
||||
const query: ConfigKeyQuery = { key: key, is_secret: is_secret };
|
||||
await removeConfig({
|
||||
body: query,
|
||||
});
|
||||
await reloadConfig();
|
||||
},
|
||||
[reloadConfig]
|
||||
);
|
||||
|
||||
const addExtension = useCallback(
|
||||
async (name: string, config: ExtensionConfig, enabled: boolean) => {
|
||||
// remove shims if present
|
||||
if (config.type === 'stdio') {
|
||||
config.cmd = removeShims(config.cmd);
|
||||
}
|
||||
const query: ExtensionQuery = { name, config, enabled };
|
||||
await apiAddExtension({
|
||||
body: query,
|
||||
});
|
||||
await reloadConfig();
|
||||
},
|
||||
[reloadConfig]
|
||||
);
|
||||
|
||||
const removeExtension = useCallback(
|
||||
async (name: string) => {
|
||||
await apiRemoveExtension({ path: { name: name } });
|
||||
await reloadConfig();
|
||||
},
|
||||
[reloadConfig]
|
||||
);
|
||||
|
||||
const getExtensions = useCallback(
|
||||
async (forceRefresh = false): Promise<FixedExtensionEntry[]> => {
|
||||
if (forceRefresh || extensionsList.length === 0) {
|
||||
const result = await apiGetExtensions();
|
||||
|
||||
if (result.response.status === 422) {
|
||||
throw new MalformedConfigError();
|
||||
}
|
||||
|
||||
if (result.error && !result.data) {
|
||||
console.log(result.error);
|
||||
return extensionsList;
|
||||
}
|
||||
|
||||
const extensionResponse: ExtensionResponse = result.data;
|
||||
setExtensionsList(extensionResponse.extensions);
|
||||
return extensionResponse.extensions;
|
||||
}
|
||||
return extensionsList;
|
||||
},
|
||||
[extensionsList]
|
||||
);
|
||||
|
||||
const toggleExtension = useCallback(
|
||||
async (name: string) => {
|
||||
const exts = await getExtensions(true);
|
||||
const extension = exts.find((ext) => ext.name === name);
|
||||
|
||||
if (extension) {
|
||||
await addExtension(name, extension, !extension.enabled);
|
||||
}
|
||||
},
|
||||
[addExtension, getExtensions]
|
||||
);
|
||||
|
||||
const getProviders = useCallback(
|
||||
async (forceRefresh = false): Promise<ProviderDetails[]> => {
|
||||
if (forceRefresh || providersList.length === 0) {
|
||||
const response = await providers();
|
||||
setProvidersList(response.data);
|
||||
return response.data;
|
||||
}
|
||||
return providersList;
|
||||
},
|
||||
[providersList]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Load all configuration data and providers on mount
|
||||
(async () => {
|
||||
@@ -95,100 +204,6 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const reloadConfig = async () => {
|
||||
const response = await readAllConfig();
|
||||
setConfig(response.data.config || {});
|
||||
};
|
||||
|
||||
const upsert = async (key: string, value: unknown, isSecret: boolean = false) => {
|
||||
const query: UpsertConfigQuery = {
|
||||
key: key,
|
||||
value: value,
|
||||
is_secret: isSecret,
|
||||
};
|
||||
await upsertConfig({
|
||||
body: query,
|
||||
});
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
const read = async (key: string, is_secret: boolean = false) => {
|
||||
const query: ConfigKeyQuery = { key: key, is_secret: is_secret };
|
||||
const response = await readConfig({
|
||||
body: query,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const remove = async (key: string, is_secret: boolean) => {
|
||||
const query: ConfigKeyQuery = { key: key, is_secret: is_secret };
|
||||
await removeConfig({
|
||||
body: query,
|
||||
});
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
const addExtension = async (name: string, config: ExtensionConfig, enabled: boolean) => {
|
||||
// remove shims if present
|
||||
if (config.type == 'stdio') {
|
||||
config.cmd = removeShims(config.cmd);
|
||||
}
|
||||
const query: ExtensionQuery = { name, config, enabled };
|
||||
await apiAddExtension({
|
||||
body: query,
|
||||
});
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
const removeExtension = async (name: string) => {
|
||||
await apiRemoveExtension({ path: { name: name } });
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
const toggleExtension = async (name: string) => {
|
||||
// Get current extensions to find the one we need to toggle
|
||||
const exts = await getExtensions(true);
|
||||
const extension = exts.find((ext) => ext.name === name);
|
||||
|
||||
if (extension) {
|
||||
// Toggle the enabled state and update using addExtension
|
||||
await addExtension(name, extension, !extension.enabled);
|
||||
}
|
||||
};
|
||||
|
||||
const getProviders = async (forceRefresh = false): Promise<ProviderDetails[]> => {
|
||||
if (forceRefresh || providersList.length === 0) {
|
||||
// If a refresh is forced or we don't have providers yet
|
||||
const response = await providers();
|
||||
setProvidersList(response.data);
|
||||
return response.data;
|
||||
}
|
||||
// Otherwise return the cached providers
|
||||
return providersList;
|
||||
};
|
||||
|
||||
const getExtensions = async (forceRefresh = false): Promise<FixedExtensionEntry[]> => {
|
||||
// If a refresh is forced, or we don't have providers yet
|
||||
if (forceRefresh || extensionsList.length === 0) {
|
||||
const result = await apiGetExtensions();
|
||||
|
||||
if (result.response.status === 422) {
|
||||
throw new MalformedConfigError();
|
||||
}
|
||||
|
||||
if (result.error && !result.data) {
|
||||
console.log(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const extensionResponse: ExtensionResponse = result.data;
|
||||
setExtensionsList(extensionResponse.extensions);
|
||||
return extensionResponse.extensions;
|
||||
}
|
||||
// Otherwise return the cached providers
|
||||
return extensionsList;
|
||||
};
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
config,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
|
||||
declare var requestAnimationFrame: (callback: FrameRequestCallback) => number;
|
||||
declare class HTMLCanvasElement {}
|
||||
@@ -51,57 +51,18 @@ const FlappyGoose: React.FC<FlappyGooseProps> = ({ onClose }) => {
|
||||
const OBSTACLE_WIDTH = 40;
|
||||
const FLAP_DURATION = 150;
|
||||
|
||||
const safeRequestAnimationFrame = (callback: FrameRequestCallback) => {
|
||||
const safeRequestAnimationFrame = useCallback((callback: FrameRequestCallback) => {
|
||||
if (typeof window !== 'undefined' && typeof requestAnimationFrame !== 'undefined') {
|
||||
requestAnimationFrame(callback);
|
||||
}
|
||||
};
|
||||
|
||||
// Load goose images
|
||||
useEffect(() => {
|
||||
const frames = [svg1, svg7];
|
||||
frames.forEach((src, index) => {
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
img.onload = () => {
|
||||
framesLoaded.current += 1;
|
||||
if (framesLoaded.current === frames.length) {
|
||||
setImagesReady(true);
|
||||
}
|
||||
};
|
||||
gooseImages.current[index] = img;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const startGame = () => {
|
||||
if (gameState.current.running || !imagesReady || typeof window === 'undefined') return;
|
||||
const handleGameOver = useCallback(() => {
|
||||
gameState.current.running = false;
|
||||
setGameOver(true);
|
||||
}, []);
|
||||
|
||||
gameState.current = {
|
||||
gooseY: CANVAS_HEIGHT / 3,
|
||||
velocity: 0,
|
||||
obstacles: [],
|
||||
gameLoop: 0,
|
||||
running: true,
|
||||
score: 0,
|
||||
isFlapping: false,
|
||||
flapEndTime: 0,
|
||||
};
|
||||
setGameOver(false);
|
||||
setDisplayScore(0);
|
||||
safeRequestAnimationFrame(gameLoop);
|
||||
};
|
||||
|
||||
const flap = () => {
|
||||
if (gameOver) {
|
||||
startGame();
|
||||
return;
|
||||
}
|
||||
gameState.current.velocity = FLAP_FORCE;
|
||||
gameState.current.isFlapping = true;
|
||||
gameState.current.flapEndTime = Date.now() + FLAP_DURATION;
|
||||
};
|
||||
|
||||
const gameLoop = () => {
|
||||
const gameLoop = useCallback(() => {
|
||||
if (!gameState.current.running || !imagesReady) return;
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
@@ -209,12 +170,63 @@ const FlappyGoose: React.FC<FlappyGooseProps> = ({ onClose }) => {
|
||||
|
||||
gameState.current.gameLoop++;
|
||||
safeRequestAnimationFrame(gameLoop);
|
||||
};
|
||||
}, [
|
||||
CANVAS_HEIGHT,
|
||||
CANVAS_WIDTH,
|
||||
GOOSE_SIZE,
|
||||
GOOSE_X,
|
||||
GRAVITY,
|
||||
OBSTACLE_GAP,
|
||||
OBSTACLE_SPEED,
|
||||
OBSTACLE_WIDTH,
|
||||
handleGameOver,
|
||||
imagesReady,
|
||||
safeRequestAnimationFrame,
|
||||
]);
|
||||
|
||||
const handleGameOver = () => {
|
||||
gameState.current.running = false;
|
||||
setGameOver(true);
|
||||
};
|
||||
const startGame = useCallback(() => {
|
||||
if (gameState.current.running || !imagesReady || typeof window === 'undefined') return;
|
||||
|
||||
gameState.current = {
|
||||
gooseY: CANVAS_HEIGHT / 3,
|
||||
velocity: 0,
|
||||
obstacles: [],
|
||||
gameLoop: 0,
|
||||
running: true,
|
||||
score: 0,
|
||||
isFlapping: false,
|
||||
flapEndTime: 0,
|
||||
};
|
||||
setGameOver(false);
|
||||
setDisplayScore(0);
|
||||
safeRequestAnimationFrame(gameLoop);
|
||||
}, [CANVAS_HEIGHT, imagesReady, safeRequestAnimationFrame, gameLoop]);
|
||||
|
||||
const flap = useCallback(() => {
|
||||
if (gameOver) {
|
||||
startGame();
|
||||
return;
|
||||
}
|
||||
gameState.current.velocity = FLAP_FORCE;
|
||||
gameState.current.isFlapping = true;
|
||||
gameState.current.flapEndTime = Date.now() + FLAP_DURATION;
|
||||
}, [FLAP_DURATION, FLAP_FORCE, gameOver, startGame]);
|
||||
|
||||
// Load goose images
|
||||
useEffect(() => {
|
||||
const frames = [svg1, svg7];
|
||||
frames.forEach((src, index) => {
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
img.onload = () => {
|
||||
framesLoaded.current += 1;
|
||||
if (framesLoaded.current === frames.length) {
|
||||
setImagesReady(true);
|
||||
}
|
||||
};
|
||||
gooseImages.current[index] = img;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
@@ -240,7 +252,7 @@ const FlappyGoose: React.FC<FlappyGooseProps> = ({ onClose }) => {
|
||||
window.removeEventListener('keydown', handleKeyPress);
|
||||
gameState.current.running = false;
|
||||
};
|
||||
}, [imagesReady]);
|
||||
}, [CANVAS_HEIGHT, CANVAS_WIDTH, flap, imagesReady, startGame]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -77,12 +77,22 @@ export default function GooseMessage({
|
||||
useEffect(() => {
|
||||
// If the message is the last message in the resumed session and has tool confirmation, it means the tool confirmation
|
||||
// is broken or cancelled, to contonue use the session, we need to append a tool response to avoid mismatch tool result error.
|
||||
if (messageIndex == messageHistoryIndex - 1 && hasToolConfirmation) {
|
||||
if (
|
||||
messageIndex === messageHistoryIndex - 1 &&
|
||||
hasToolConfirmation &&
|
||||
toolConfirmationContent
|
||||
) {
|
||||
appendMessage(
|
||||
createToolErrorResponseMessage(toolConfirmationContent.id, 'The tool call is cancelled.')
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
}, [
|
||||
messageIndex,
|
||||
messageHistoryIndex,
|
||||
hasToolConfirmation,
|
||||
toolConfirmationContent,
|
||||
appendMessage,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="goose-message flex w-[90%] justify-start opacity-0 animate-[appear_150ms_ease-in_forwards]">
|
||||
|
||||
@@ -42,36 +42,33 @@ export default function Input({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Debounced function to update actual value
|
||||
const debouncedSetValue = useCallback(
|
||||
debounce((val: string) => {
|
||||
setValue(val);
|
||||
}, 150),
|
||||
[]
|
||||
);
|
||||
|
||||
// Debounced autosize function
|
||||
const debouncedAutosize = useCallback(
|
||||
debounce((textArea: HTMLTextAreaElement, value: string) => {
|
||||
textArea.style.height = '0px'; // Reset height
|
||||
const scrollHeight = textArea.scrollHeight;
|
||||
textArea.style.height = Math.min(scrollHeight, maxHeight) + 'px';
|
||||
}, 150),
|
||||
[]
|
||||
);
|
||||
|
||||
const useAutosizeTextArea = (textAreaRef: HTMLTextAreaElement | null, value: string) => {
|
||||
useEffect(() => {
|
||||
if (textAreaRef) {
|
||||
debouncedAutosize(textAreaRef, value);
|
||||
}
|
||||
}, [textAreaRef, value]);
|
||||
};
|
||||
|
||||
const minHeight = '1rem';
|
||||
const maxHeight = 10 * 24;
|
||||
|
||||
useAutosizeTextArea(textAreaRef.current, displayValue);
|
||||
// Debounced function to update actual value
|
||||
const debouncedSetValue = useCallback((val: string) => {
|
||||
debounce((value: string) => {
|
||||
setValue(value);
|
||||
}, 150)(val);
|
||||
}, []);
|
||||
|
||||
// Debounced autosize function
|
||||
const debouncedAutosize = useCallback(
|
||||
(textArea: HTMLTextAreaElement) => {
|
||||
debounce((element: HTMLTextAreaElement) => {
|
||||
element.style.height = '0px'; // Reset height
|
||||
const scrollHeight = element.scrollHeight;
|
||||
element.style.height = Math.min(scrollHeight, maxHeight) + 'px';
|
||||
}, 150)(textArea);
|
||||
},
|
||||
[maxHeight]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (textAreaRef.current) {
|
||||
debouncedAutosize(textAreaRef.current);
|
||||
}
|
||||
}, [debouncedAutosize, displayValue]);
|
||||
|
||||
const handleChange = (evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const val = evt.target.value;
|
||||
@@ -82,17 +79,17 @@ export default function Input({
|
||||
// Cleanup debounced functions on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedSetValue.cancel();
|
||||
debouncedAutosize.cancel();
|
||||
debouncedSetValue.cancel?.();
|
||||
debouncedAutosize.cancel?.();
|
||||
};
|
||||
}, []);
|
||||
}, [debouncedSetValue, debouncedAutosize]);
|
||||
|
||||
// Handlers for composition events, which are crucial for proper IME behavior
|
||||
const handleCompositionStart = (evt: React.CompositionEvent<HTMLTextAreaElement>) => {
|
||||
const handleCompositionStart = () => {
|
||||
setIsComposing(true);
|
||||
};
|
||||
|
||||
const handleCompositionEnd = (evt: React.CompositionEvent<HTMLTextAreaElement>) => {
|
||||
const handleCompositionEnd = () => {
|
||||
setIsComposing(false);
|
||||
};
|
||||
|
||||
@@ -118,7 +115,7 @@ export default function Input({
|
||||
}
|
||||
}
|
||||
|
||||
if (newIndex == historyIndex) {
|
||||
if (newIndex === historyIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -231,7 +228,7 @@ export default function Input({
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onStop();
|
||||
onStop?.();
|
||||
}}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 [&_svg]:size-5 text-textSubtle hover:text-textStandard"
|
||||
>
|
||||
|
||||
@@ -4,15 +4,14 @@ import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
||||
import { Check, Copy } from './icons';
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
function rehypeinlineCodeProperty() {
|
||||
return function (tree) {
|
||||
if (!tree) return;
|
||||
visit(tree, 'element', function (node, index, parent) {
|
||||
if (node.tagName == 'code' && parent && parent.tagName === 'pre') {
|
||||
visit(tree, 'element', function (node) {
|
||||
if (node.tagName == 'code' && node.parent && node.parent.tagName === 'pre') {
|
||||
node.properties.inlinecode = 'false';
|
||||
} else {
|
||||
node.properties.inlinecode = 'true';
|
||||
@@ -75,8 +74,6 @@ const CodeBlock = ({ language, children }: { language: string; children: string
|
||||
};
|
||||
|
||||
export default function MarkdownContent({ content, className = '' }: MarkdownContentProps) {
|
||||
// Determine whether dark mode is enabled
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
return (
|
||||
<div className="w-full overflow-x-hidden">
|
||||
<ReactMarkdown
|
||||
@@ -100,8 +97,8 @@ export default function MarkdownContent({ content, className = '' }: MarkdownCon
|
||||
|
||||
${className}`}
|
||||
components={{
|
||||
a: ({ node, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />,
|
||||
code({ node, className, children, inlinecode, ...props }) {
|
||||
a: ({ ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />,
|
||||
code({ className, children, inlinecode, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || 'language-text');
|
||||
return inlinecode == 'false' && match ? (
|
||||
<CodeBlock language={match[1]}>{String(children).replace(/\n$/, '')}</CodeBlock>
|
||||
|
||||
@@ -2,8 +2,16 @@ import { ChevronUp } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
import MarkdownContent from './MarkdownContent';
|
||||
|
||||
type ToolCallArgumentValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| ToolCallArgumentValue[]
|
||||
| { [key: string]: ToolCallArgumentValue };
|
||||
|
||||
interface ToolCallArgumentsProps {
|
||||
args: Record<string, any>;
|
||||
args: Record<string, ToolCallArgumentValue>;
|
||||
}
|
||||
|
||||
export function ToolCallArguments({ args }: ToolCallArgumentsProps) {
|
||||
@@ -13,7 +21,7 @@ export function ToolCallArguments({ args }: ToolCallArgumentsProps) {
|
||||
setExpandedKeys((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||
};
|
||||
|
||||
const renderValue = (key: string, value: any) => {
|
||||
const renderValue = (key: string, value: ToolCallArgumentValue) => {
|
||||
if (typeof value === 'string') {
|
||||
const needsExpansion = value.length > 60;
|
||||
const isExpanded = expandedKeys[key];
|
||||
@@ -45,18 +53,12 @@ export function ToolCallArguments({ args }: ToolCallArgumentsProps) {
|
||||
onClick={() => toggleKey(key)}
|
||||
className="text-sm hover:opacity-75 text-textStandard"
|
||||
>
|
||||
{/* {isExpanded ? '▼ ' : '▶ '} */}
|
||||
<ChevronUp
|
||||
className={`h-5 w-5 transition-all origin-center ${!isExpanded ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* {isExpanded && (
|
||||
<div className="mt-2">
|
||||
<MarkdownContent content={value} />
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,21 +44,25 @@ export const SearchBar: React.FC<SearchBarProps> = ({
|
||||
|
||||
// Create debounced search function
|
||||
const debouncedSearch = useCallback(
|
||||
debounce((term: string, caseSensitive: boolean) => {
|
||||
onSearch(term, caseSensitive);
|
||||
}, 150),
|
||||
[]
|
||||
(term: string, isCaseSensitive: boolean) => {
|
||||
debounce((searchTerm: string, caseSensitive: boolean) => {
|
||||
onSearch(searchTerm, caseSensitive);
|
||||
}, 150)(term, isCaseSensitive);
|
||||
},
|
||||
[onSearch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
|
||||
// Cleanup debounced function
|
||||
return () => {
|
||||
debouncedSearch.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Cleanup debounced function on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedSearch.cancel?.();
|
||||
};
|
||||
}, [debouncedSearch]);
|
||||
|
||||
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
setDisplayTerm(value); // Update display immediately
|
||||
@@ -93,7 +97,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
|
||||
|
||||
const handleClose = () => {
|
||||
setIsExiting(true);
|
||||
debouncedSearch.cancel(); // Cancel any pending searches
|
||||
debouncedSearch.cancel?.(); // Cancel any pending searches
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 150); // Match animation duration
|
||||
|
||||
@@ -36,20 +36,25 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
|
||||
|
||||
// Create debounced highlight function
|
||||
const debouncedHighlight = useCallback(
|
||||
debounce((term: string, caseSensitive: boolean, highlighter: SearchHighlighter) => {
|
||||
const highlights = highlighter.highlight(term, caseSensitive);
|
||||
const count = highlights.length;
|
||||
(term: string, caseSensitive: boolean, highlighter: SearchHighlighter) => {
|
||||
debounce(
|
||||
(searchTerm: string, isCaseSensitive: boolean, searchHighlighter: SearchHighlighter) => {
|
||||
const highlights = searchHighlighter.highlight(searchTerm, isCaseSensitive);
|
||||
const count = highlights.length;
|
||||
|
||||
if (count > 0) {
|
||||
setSearchResults({
|
||||
currentIndex: 1,
|
||||
count,
|
||||
});
|
||||
highlighter.setCurrentMatch(0, true); // Explicitly scroll when setting initial match
|
||||
} else {
|
||||
setSearchResults(null);
|
||||
}
|
||||
}, 150),
|
||||
if (count > 0) {
|
||||
setSearchResults({
|
||||
currentIndex: 1,
|
||||
count,
|
||||
});
|
||||
searchHighlighter.setCurrentMatch(0, true); // Explicitly scroll when setting initial match
|
||||
} else {
|
||||
setSearchResults(null);
|
||||
}
|
||||
},
|
||||
150
|
||||
)(term, caseSensitive, highlighter);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -60,9 +65,9 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
|
||||
highlighterRef.current.destroy();
|
||||
highlighterRef.current = null;
|
||||
}
|
||||
debouncedHighlight.cancel();
|
||||
debouncedHighlight.cancel?.();
|
||||
};
|
||||
}, []);
|
||||
}, [debouncedHighlight]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -162,7 +167,7 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
|
||||
highlighterRef.current.clearHighlights();
|
||||
}
|
||||
// Cancel any pending highlight operations
|
||||
debouncedHighlight.cancel();
|
||||
debouncedHighlight.cancel?.();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
PopoverTrigger,
|
||||
} from '../../components/ui/popover';
|
||||
import { Popover, PopoverContent, PopoverPortal, PopoverTrigger } from '../ui/popover';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ChatSmart, Idea, More, Refresh, Time, Send } from '../icons';
|
||||
import { FolderOpen, Moon, Sliders, Sun } from 'lucide-react';
|
||||
import { View } from '../../App';
|
||||
import { useConfig } from '../ConfigContext';
|
||||
import { settingsV2Enabled } from '../../flags';
|
||||
|
||||
interface VersionInfo {
|
||||
current_version: string;
|
||||
available_versions: string[];
|
||||
}
|
||||
import { ViewOptions, View } from '../../App';
|
||||
|
||||
interface MenuButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -105,13 +95,11 @@ export default function MoreMenu({
|
||||
setView,
|
||||
setIsGoosehintsModalOpen,
|
||||
}: {
|
||||
setView: (view: View) => void;
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { remove } = useConfig();
|
||||
const [versions, setVersions] = useState<VersionInfo | null>(null);
|
||||
const [showVersions, setShowVersions] = useState(false);
|
||||
const [themeMode, setThemeMode] = useState<'light' | 'dark' | 'system'>(() => {
|
||||
const savedUseSystemTheme = localStorage.getItem('use_system_theme') === 'true';
|
||||
if (savedUseSystemTheme) {
|
||||
@@ -129,27 +117,6 @@ export default function MoreMenu({
|
||||
return themeMode === 'dark';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch available versions when the menu opens
|
||||
const fetchVersions = async () => {
|
||||
try {
|
||||
const port = window.appConfig.get('GOOSE_PORT');
|
||||
const response = await fetch(`http://127.0.0.1:${port}/agent/versions`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setVersions(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch versions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchVersions();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
@@ -196,6 +163,8 @@ export default function MoreMenu({
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className={`z-[100] absolute top-2 right-4 w-[20px] h-[20px] transition-colors cursor-pointer no-drag hover:text-textProminent ${open ? 'text-textProminent' : 'text-textSubtle'}`}
|
||||
role="button"
|
||||
aria-label="More options"
|
||||
>
|
||||
<More />
|
||||
</button>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import MoreMenu from './MoreMenu';
|
||||
import React from 'react';
|
||||
import type { View } from '../../App';
|
||||
import { View, ViewOptions } from '../../App';
|
||||
|
||||
export default function MoreMenuLayout({
|
||||
setView,
|
||||
setIsGoosehintsModalOpen,
|
||||
}: {
|
||||
setView: (view: View, viewOptions?: Record<any, any>) => void;
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
|
||||
@@ -39,7 +39,6 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
||||
const [isSharing, setIsSharing] = useState(false);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [canShare, setCanShare] = useState(false);
|
||||
const [shareError, setShareError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const savedSessionConfig = localStorage.getItem('session_sharing_config');
|
||||
@@ -58,7 +57,6 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
||||
|
||||
const handleShare = async () => {
|
||||
setIsSharing(true);
|
||||
setShareError(null);
|
||||
|
||||
try {
|
||||
// Get the session sharing configuration from localStorage
|
||||
@@ -87,7 +85,6 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
||||
setIsShareModalOpen(true);
|
||||
} catch (error) {
|
||||
console.error('Error sharing session:', error);
|
||||
setShareError(error instanceof Error ? error.message : 'Unknown error occurred');
|
||||
toast.error(
|
||||
`Failed to share session: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ViewConfig } from '../../App';
|
||||
import {
|
||||
MessageSquareText,
|
||||
Target,
|
||||
@@ -14,9 +13,10 @@ import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import BackButton from '../ui/BackButton';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { View, ViewOptions } from '../../App';
|
||||
|
||||
interface SessionListViewProps {
|
||||
setView: (view: ViewConfig['view'], viewOptions?: Record<any, any>) => void;
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
onSelectSession: (sessionId: string) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ViewConfig } from '../../App';
|
||||
import { View, ViewOptions } from '../../App';
|
||||
import { fetchSessionDetails, type SessionDetails } from '../../sessions';
|
||||
import SessionListView from './SessionListView';
|
||||
import SessionHistoryView from './SessionHistoryView';
|
||||
import { toastError } from '../../toasts';
|
||||
|
||||
interface SessionsViewProps {
|
||||
setView: (view: ViewConfig['view'], viewOptions?: Record<any, any>) => void;
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
}
|
||||
|
||||
const SessionsView: React.FC<SessionsViewProps> = ({ setView }) => {
|
||||
|
||||
@@ -23,8 +23,10 @@ interface OllamaBattleGameProps {
|
||||
}
|
||||
|
||||
export function OllamaBattleGame({ onComplete, _requiredKeys }: OllamaBattleGameProps) {
|
||||
// Use type assertion for audioRef to avoid DOM lib dependency
|
||||
const audioRef = useRef<any>(null);
|
||||
// Use Audio element type for audioRef
|
||||
const audioRef = useRef<{ play: () => Promise<void>; pause: () => void; volume: number } | null>(
|
||||
null
|
||||
);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
|
||||
const [battleState, setBattleState] = useState<BattleState>({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IpcRendererEvent } from 'electron';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { Settings as SettingsType } from './types';
|
||||
import {
|
||||
@@ -13,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 '../../App';
|
||||
import { View, ViewOptions } from '../../App';
|
||||
import { ModeSelection } from './basic/ModeSelection';
|
||||
import SessionSharingSection from './session/SessionSharingSection';
|
||||
import { toastSuccess } from '../../toasts';
|
||||
@@ -59,7 +60,7 @@ export default function SettingsView({
|
||||
viewOptions,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
setView: (view: View) => void;
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
viewOptions: SettingsViewOptions;
|
||||
}) {
|
||||
const [settings, setSettings] = React.useState<SettingsType>(() => {
|
||||
@@ -92,7 +93,7 @@ export default function SettingsView({
|
||||
|
||||
// Listen for settings updates from extension storage
|
||||
useEffect(() => {
|
||||
const handleSettingsUpdate = (_: any) => {
|
||||
const handleSettingsUpdate = (_event: IpcRendererEvent) => {
|
||||
const saved = localStorage.getItem('user_settings');
|
||||
if (saved) {
|
||||
let currentSettings = JSON.parse(saved);
|
||||
|
||||
@@ -101,8 +101,18 @@ export async function getProvidersList(): Promise<Provider[]> {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
interface ProviderItem {
|
||||
id: string;
|
||||
details?: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
models?: string[];
|
||||
required_keys?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// Format the response into an array of providers
|
||||
return data.map((item: any) => ({
|
||||
return data.map((item: ProviderItem) => ({
|
||||
id: item.id, // Root-level ID
|
||||
name: item.details?.name || 'Unknown Provider', // Nested name in details
|
||||
description: item.details?.description || 'No description available.', // Nested description
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Button } from '../../ui/button';
|
||||
import { Input } from '../../ui/input';
|
||||
import { FullExtensionConfig, DEFAULT_EXTENSION_TIMEOUT } from '../../../extensions';
|
||||
import { Select } from '../../ui/Select';
|
||||
import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles';
|
||||
import { getApiUrl, getSecretKey } from '../../../config';
|
||||
import { toastError } from '../../../toasts';
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Model } from './ModelContext';
|
||||
|
||||
export const openai_models = ['gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo', 'o1'];
|
||||
|
||||
export const anthropic_models = [
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { Check, Plus, Settings, X, Rocket } from 'lucide-react';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/Tooltip';
|
||||
import { Portal } from '@radix-ui/react-portal';
|
||||
import { required_keys } from '../models/hardcoded_stuff';
|
||||
import { useActiveKeys } from '../api_keys/ActiveKeysContext';
|
||||
import { getActiveProviders } from '../api_keys/utils';
|
||||
|
||||
// Common interfaces and helper functions
|
||||
interface Provider {
|
||||
@@ -67,7 +65,6 @@ function BaseProviderCard({
|
||||
}: BaseProviderCardProps) {
|
||||
const numRequiredKeys = required_keys[name]?.length || 0;
|
||||
const tooltipText = numRequiredKeys === 1 ? `Add ${name} API Key` : `Add ${name} API Keys`;
|
||||
const { activeKeys, setActiveKeys } = useActiveKeys();
|
||||
|
||||
return (
|
||||
<div className="relative h-full p-[2px] overflow-hidden rounded-[9px] group/card bg-borderSubtle hover:bg-transparent hover:duration-300">
|
||||
|
||||
@@ -4,7 +4,6 @@ import { BaseProviderGrid, getProviderDescription } from './BaseProviderGrid';
|
||||
import { supported_providers, provider_aliases, required_keys } from '../models/hardcoded_stuff';
|
||||
import { ProviderSetupModal } from '../ProviderSetupModal';
|
||||
import { getApiUrl, getSecretKey } from '../../../config';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getActiveProviders, isSecretKey } from '../api_keys/utils';
|
||||
import { useModel } from '../models/ModelContext';
|
||||
import { Button } from '../../ui/button';
|
||||
|
||||
@@ -15,11 +15,9 @@ export type SettingsViewOptions = {
|
||||
export default function SettingsView({
|
||||
onClose,
|
||||
setView,
|
||||
viewOptions,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
setView: (view: View) => void;
|
||||
viewOptions: SettingsViewOptions;
|
||||
}) {
|
||||
return (
|
||||
<div className="h-screen w-full animate-[fadein_200ms_ease-in_forwards]">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { GPSIcon } from '../../ui/icons';
|
||||
@@ -17,39 +17,29 @@ import { activateExtension, deleteExtension, toggleExtension, updateExtension }
|
||||
|
||||
export default function ExtensionsSection() {
|
||||
const { getExtensions, addExtension, removeExtension } = useConfig();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [extensions, setExtensions] = useState<FixedExtensionEntry[]>([]);
|
||||
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
// We don't need errorFormData anymore since we're not reopening modals on failure
|
||||
|
||||
const fetchExtensions = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const extensionsList = await getExtensions(true); // Force refresh
|
||||
// Sort extensions by name to maintain consistent order
|
||||
const sortedExtensions = [...extensionsList].sort((a, b) => a.name.localeCompare(b.name));
|
||||
setExtensions(sortedExtensions);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError('Failed to load extensions');
|
||||
console.error('Error loading extensions:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const fetchExtensions = useCallback(async () => {
|
||||
const extensionsList = await getExtensions(true); // Force refresh
|
||||
// Sort extensions by name to maintain consistent order
|
||||
const sortedExtensions = [...extensionsList].sort((a, b) => a.name.localeCompare(b.name));
|
||||
setExtensions(sortedExtensions);
|
||||
}, [getExtensions]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchExtensions();
|
||||
}, []);
|
||||
}, [fetchExtensions]);
|
||||
|
||||
const handleExtensionToggle = async (extension: FixedExtensionEntry) => {
|
||||
// If extension is enabled, we are trying to toggle if off, otherwise on
|
||||
const toggleDirection = extension.enabled ? 'toggleOff' : 'toggleOn';
|
||||
const extensionConfig = extractExtensionConfig(extension);
|
||||
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
await toggleExtension({
|
||||
toggle: toggleDirection,
|
||||
|
||||
@@ -3,12 +3,17 @@ import { getApiUrl, getSecretKey } from '../../../config';
|
||||
import { toastService, ToastServiceOptions } from '../../../toasts';
|
||||
import { replaceWithShims } from './utils';
|
||||
|
||||
interface ApiResponse {
|
||||
error?: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an API call to the extension endpoints
|
||||
*/
|
||||
export async function extensionApiCall(
|
||||
endpoint: string,
|
||||
payload: any,
|
||||
payload: ExtensionConfig | string,
|
||||
options: ToastServiceOptions = {}
|
||||
): Promise<Response> {
|
||||
// Configure toast notifications
|
||||
@@ -118,7 +123,7 @@ function handleErrorResponse(
|
||||
}
|
||||
|
||||
// Safely parses JSON response
|
||||
async function parseResponseData(response: Response): Promise<any> {
|
||||
async function parseResponseData(response: Response): Promise<ApiResponse> {
|
||||
try {
|
||||
const text = await response.text();
|
||||
return text ? JSON.parse(text) : { error: false };
|
||||
|
||||
@@ -7,6 +7,46 @@ interface ActivateExtensionProps {
|
||||
extensionConfig: ExtensionConfig;
|
||||
}
|
||||
|
||||
type ExtensionError = {
|
||||
message?: string;
|
||||
code?: number;
|
||||
name?: string;
|
||||
stack?: string;
|
||||
};
|
||||
|
||||
type RetryOptions = {
|
||||
retries?: number;
|
||||
delayMs?: number;
|
||||
shouldRetry?: (error: ExtensionError, attempt: number) => boolean;
|
||||
backoffFactor?: number; // multiplier for exponential backoff
|
||||
};
|
||||
|
||||
async function retryWithBackoff<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
|
||||
const { retries = 3, delayMs = 1000, backoffFactor = 1.5, shouldRetry = () => true } = options;
|
||||
|
||||
let attempt = 0;
|
||||
let lastError: ExtensionError;
|
||||
|
||||
while (attempt <= retries) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
lastError = err as ExtensionError;
|
||||
attempt++;
|
||||
|
||||
if (attempt > retries || !shouldRetry(lastError, attempt)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const waitTime = delayMs * Math.pow(backoffFactor, attempt - 1);
|
||||
console.warn(`Retry attempt ${attempt} failed. Retrying in ${waitTime}ms...`, err);
|
||||
await new Promise((res) => setTimeout(res, waitTime));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates an extension by adding it to both the config system and the API.
|
||||
* @param props The extension activation properties
|
||||
@@ -43,39 +83,6 @@ export async function activateExtension({
|
||||
}
|
||||
}
|
||||
|
||||
type RetryOptions = {
|
||||
retries?: number;
|
||||
delayMs?: number;
|
||||
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
||||
backoffFactor?: number; // multiplier for exponential backoff
|
||||
};
|
||||
|
||||
async function retryWithBackoff<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
|
||||
const { retries = 3, delayMs = 1000, backoffFactor = 1.5, shouldRetry = () => true } = options;
|
||||
|
||||
let attempt = 0;
|
||||
let lastError: unknown;
|
||||
|
||||
while (attempt <= retries) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
attempt++;
|
||||
|
||||
if (attempt > retries || !shouldRetry(err, attempt)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const waitTime = delayMs * Math.pow(backoffFactor, attempt - 1);
|
||||
console.warn(`Retry attempt ${attempt} failed. Retrying in ${waitTime}ms...`, err);
|
||||
await new Promise((res) => setTimeout(res, waitTime));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
interface AddToAgentOnStartupProps {
|
||||
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
|
||||
extensionConfig: ExtensionConfig;
|
||||
@@ -92,7 +99,7 @@ export async function addToAgentOnStartup({
|
||||
await retryWithBackoff(() => addToAgent(extensionConfig, { silent: true }), {
|
||||
retries: 3,
|
||||
delayMs: 1000,
|
||||
shouldRetry: (error: any) =>
|
||||
shouldRetry: (error: ExtensionError) =>
|
||||
error.message &&
|
||||
(error.message.includes('428') ||
|
||||
error.message.includes('Precondition Required') ||
|
||||
@@ -103,7 +110,7 @@ export async function addToAgentOnStartup({
|
||||
toastService.error({
|
||||
title: extensionConfig.name,
|
||||
msg: 'Extension failed to start and will be disabled.',
|
||||
traceback: finalError,
|
||||
traceback: finalError as Error,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,7 @@ interface ExtensionConfigFieldsProps {
|
||||
type: 'stdio' | 'sse' | 'builtin';
|
||||
full_cmd: string;
|
||||
endpoint: string;
|
||||
onChange: (key: string, value: any) => void;
|
||||
onChange: (key: string, value: string) => void;
|
||||
submitAttempted?: boolean;
|
||||
isValid?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Input } from '../../../ui/input';
|
||||
import { Select } from '../../../ui/Select';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
interface ExtensionInfoFieldsProps {
|
||||
name: string;
|
||||
type: 'stdio' | 'sse' | 'builtin';
|
||||
description: string;
|
||||
onChange: (key: string, value: any) => void;
|
||||
onChange: (key: string, value: string) => void;
|
||||
submitAttempted: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Input } from '../../../ui/input';
|
||||
import Select from 'react-select';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
interface ExtensionTimeoutFieldProps {
|
||||
timeout: number;
|
||||
onChange: (key: string, value: any) => void;
|
||||
onChange: (key: string, value: string | number) => void;
|
||||
submitAttempted: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Default extension timeout in seconds
|
||||
// TODO: keep in sync with rust better
|
||||
import * as module from 'node:module';
|
||||
|
||||
export const DEFAULT_EXTENSION_TIMEOUT = 300;
|
||||
|
||||
@@ -132,7 +131,8 @@ export function combineCmdAndArgs(cmd: string, args: string[]): string {
|
||||
* @returns The ExtensionConfig portion of the object
|
||||
*/
|
||||
export function extractExtensionConfig(fixedEntry: FixedExtensionEntry): ExtensionConfig {
|
||||
const { enabled, ...extensionConfig } = fixedEntry;
|
||||
// todo: enabled not used?
|
||||
const { ...extensionConfig } = fixedEntry;
|
||||
return extensionConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getApiUrl, getSecretKey } from '../../../config';
|
||||
import { all_goose_modes, filterGooseModes, ModeSelectionItem } from './ModeSelectionItem';
|
||||
import ExtensionList from '@/src/components/settings_v2/extensions/subcomponents/ExtensionList';
|
||||
import { Button } from '@/src/components/ui/button';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { GPSIcon } from '@/src/components/ui/icons';
|
||||
|
||||
export const ModeSection = () => {
|
||||
const [currentMode, setCurrentMode] = useState('auto');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import type { View } from '../../../App';
|
||||
import ModelSettingsButtons from './subcomponents/ModelSettingsButtons';
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
@@ -16,7 +16,7 @@ export default function ModelsSection({ setView }: ModelsSectionProps) {
|
||||
const { read, getProviders } = useConfig();
|
||||
|
||||
// Function to load model data
|
||||
const loadModelData = async () => {
|
||||
const loadModelData = useCallback(async () => {
|
||||
try {
|
||||
const gooseModel = (await read('GOOSE_MODEL', false)) as string;
|
||||
const gooseProvider = (await read('GOOSE_PROVIDER', false)) as string;
|
||||
@@ -40,7 +40,7 @@ export default function ModelsSection({ setView }: ModelsSectionProps) {
|
||||
} catch (error) {
|
||||
console.error('Error loading model data:', error);
|
||||
}
|
||||
};
|
||||
}, [read, getProviders]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initial load
|
||||
@@ -55,7 +55,7 @@ export default function ModelsSection({ setView }: ModelsSectionProps) {
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
}, [loadModelData]);
|
||||
|
||||
return (
|
||||
<section id="models" className="px-8">
|
||||
|
||||
@@ -4,10 +4,10 @@ import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useConfig } from '../../../ConfigContext';
|
||||
import { getCurrentModelAndProviderForDisplay } from '../index';
|
||||
import { AddModelModal } from '../subcomponents/AddModelModal';
|
||||
import type { View } from '../../../../App';
|
||||
import { View } from '../../../../App';
|
||||
|
||||
interface ModelsBottomBarProps {
|
||||
dropdownRef: any;
|
||||
dropdownRef: React.RefObject<HTMLDivElement>;
|
||||
setView: (view: View) => void;
|
||||
}
|
||||
export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBarProps) {
|
||||
|
||||
@@ -72,7 +72,7 @@ export function BaseModelsList({
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [read]);
|
||||
}, [read, modelList, upsert]);
|
||||
|
||||
const handleModelSelection = async (model: Model) => {
|
||||
await changeModel({ model: model, writeToConfig: upsert, getExtensions, addExtension });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Model from '../modelInterface';
|
||||
|
||||
const MAX_RECENT_MODELS = 3;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ArrowLeftRight, ExternalLink, Plus } from 'lucide-react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { ArrowLeftRight, ExternalLink } from 'lucide-react';
|
||||
|
||||
import Modal from '../../../Modal';
|
||||
import { Button } from '../../../ui/button';
|
||||
@@ -11,7 +11,7 @@ import { changeModel } from '../index';
|
||||
import type { View } from '../../../../App';
|
||||
import Model, { getProviderMetadata } from '../modelInterface';
|
||||
|
||||
const ModalButtons = ({ onSubmit, onCancel, isValid, validationErrors }) => (
|
||||
const ModalButtons = ({ onSubmit, onCancel, _isValid, _validationErrors }) => (
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -51,7 +51,7 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
|
||||
const [attemptedSubmit, setAttemptedSubmit] = useState(false);
|
||||
|
||||
// Validate form data
|
||||
const validateForm = () => {
|
||||
const validateForm = useCallback(() => {
|
||||
const errors = {
|
||||
provider: '',
|
||||
model: '',
|
||||
@@ -71,7 +71,7 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
|
||||
setValidationErrors(errors);
|
||||
setIsValid(formIsValid);
|
||||
return formIsValid;
|
||||
};
|
||||
}, [model, provider]);
|
||||
|
||||
const onSubmit = async () => {
|
||||
setAttemptedSubmit(true);
|
||||
@@ -96,7 +96,7 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
|
||||
if (attemptedSubmit) {
|
||||
validateForm();
|
||||
}
|
||||
}, [provider, model, attemptedSubmit]);
|
||||
}, [provider, model, attemptedSubmit, validateForm]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
||||
@@ -72,7 +72,13 @@ const ProviderCards = memo(function ProviderCards({
|
||||
isOnboarding={isOnboarding}
|
||||
/>
|
||||
));
|
||||
}, [providers, isOnboarding, configureProviderViaModal, onProviderLaunch]);
|
||||
}, [
|
||||
providers,
|
||||
isOnboarding,
|
||||
configureProviderViaModal,
|
||||
deleteProviderConfigViaModal,
|
||||
onProviderLaunch,
|
||||
]);
|
||||
|
||||
return <>{providerCards}</>;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import ProviderDetails from './interfaces/ProviderDetails';
|
||||
import OllamaForm from './modal/subcomponents/forms/OllamaForm';
|
||||
import OllamaSubmitHandler from './modal/subcomponents/handlers/OllamaSubmitHandler';
|
||||
|
||||
export interface ProviderRegistry {
|
||||
name: string;
|
||||
|
||||
@@ -7,7 +7,6 @@ import { ProviderDetails } from '../../../api/types.gen';
|
||||
import { initializeSystem } from '../../../utils/providerUtils';
|
||||
import WelcomeGooseLogo from '../../WelcomeGooseLogo';
|
||||
import { toastService } from '../../../toasts';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
interface ProviderSettingsProps {
|
||||
onClose: () => void;
|
||||
@@ -84,7 +83,7 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
[onClose, upsert]
|
||||
[onClose, upsert, getExtensions, addExtension]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import ParameterSchema from '../interfaces/ParameterSchema';
|
||||
import ProviderSetupFormProps from '../modal/interfaces/ProviderSetupFormProps';
|
||||
|
||||
@@ -8,5 +9,5 @@ export default interface ProviderDetails {
|
||||
parameters: ParameterSchema[];
|
||||
getTags?: (name: string) => string[];
|
||||
customForm?: React.ComponentType<ProviderSetupFormProps>;
|
||||
customSubmit?: (e: any) => void;
|
||||
customSubmit?: (e: React.SyntheticEvent) => void;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
interface ProviderMetadata {
|
||||
[key: string]: string | number | boolean | null;
|
||||
}
|
||||
|
||||
// runtime data per instance
|
||||
export default interface ProviderState {
|
||||
id: string;
|
||||
name: string;
|
||||
isConfigured: boolean;
|
||||
metadata: any;
|
||||
metadata: ProviderMetadata;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import { ProviderDetails } from '../../../../api/types.gen';
|
||||
|
||||
interface FormValues {
|
||||
[key: string]: string | number | boolean | null;
|
||||
}
|
||||
|
||||
interface ModalProps {
|
||||
onSubmit?: (values: any) => void;
|
||||
onSubmit?: (values: FormValues) => void;
|
||||
onCancel?: () => void;
|
||||
onDelete?: (values: any) => void;
|
||||
formProps?: any;
|
||||
onDelete?: (values: FormValues) => void;
|
||||
formProps?: {
|
||||
initialValues?: FormValues;
|
||||
validationSchema?: object;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProviderModalContextType {
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { SyntheticEvent } from 'react';
|
||||
import { Button } from '../../../../ui/button';
|
||||
import { Trash2, AlertTriangle } from 'lucide-react';
|
||||
|
||||
interface ProviderSetupActionsProps {
|
||||
onCancel: () => void;
|
||||
onSubmit: (e: any) => void;
|
||||
onSubmit: (e: SyntheticEvent) => void;
|
||||
onDelete?: () => void;
|
||||
showDeleteConfirmation?: boolean;
|
||||
onConfirmDelete?: () => void;
|
||||
|
||||
+78
-48
@@ -1,12 +1,39 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import { Input } from '../../../../../ui/input';
|
||||
import { useConfig } from '../../../../../ConfigContext'; // Adjust this import path as needed
|
||||
|
||||
interface ConfigParameter {
|
||||
name: string;
|
||||
required: boolean;
|
||||
secret?: boolean;
|
||||
default?: string | number | boolean | null;
|
||||
}
|
||||
|
||||
interface ProviderMetadata {
|
||||
config_keys?: ConfigParameter[];
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
known_models?: string[];
|
||||
default_model?: string;
|
||||
[key: string]: string | string[] | ConfigParameter[] | undefined;
|
||||
}
|
||||
|
||||
interface Provider {
|
||||
metadata: ProviderMetadata;
|
||||
name: string;
|
||||
is_configured: boolean;
|
||||
[key: string]: string | boolean | ProviderMetadata;
|
||||
}
|
||||
|
||||
interface ValidationErrors {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface DefaultProviderSetupFormProps {
|
||||
configValues: Record<string, any>;
|
||||
setConfigValues: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||
provider: any;
|
||||
validationErrors: any;
|
||||
configValues: Record<string, string>;
|
||||
setConfigValues: React.Dispatch<React.SetStateAction<Record<string, string>>>;
|
||||
provider: Provider;
|
||||
validationErrors: ValidationErrors;
|
||||
}
|
||||
|
||||
export default function DefaultProviderSetupForm({
|
||||
@@ -15,61 +42,64 @@ export default function DefaultProviderSetupForm({
|
||||
provider,
|
||||
validationErrors = {},
|
||||
}: DefaultProviderSetupFormProps) {
|
||||
const parameters = provider.metadata.config_keys || [];
|
||||
const parameters = useMemo(
|
||||
() => provider.metadata.config_keys || [],
|
||||
[provider.metadata.config_keys]
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { read } = useConfig();
|
||||
|
||||
console.log('configValues default form', configValues);
|
||||
|
||||
// Initialize values when the component mounts or provider changes
|
||||
useEffect(() => {
|
||||
const loadConfigValues = async () => {
|
||||
setIsLoading(true);
|
||||
const newValues = { ...configValues };
|
||||
const loadConfigValues = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
const newValues = { ...configValues };
|
||||
|
||||
// Try to load actual values from config for each parameter that is not secret
|
||||
for (const parameter of parameters) {
|
||||
if (parameter.required) {
|
||||
try {
|
||||
// Check if there's a stored value in the config system
|
||||
const configKey = `${parameter.name}`;
|
||||
const configResponse = await read(configKey, parameter.secret || false);
|
||||
// Try to load actual values from config for each parameter that is not secret
|
||||
for (const parameter of parameters) {
|
||||
if (parameter.required) {
|
||||
try {
|
||||
// Check if there's a stored value in the config system
|
||||
const configKey = `${parameter.name}`;
|
||||
const configResponse = await read(configKey, parameter.secret || false);
|
||||
|
||||
if (configResponse) {
|
||||
// Use the value from the config provider
|
||||
newValues[parameter.name] = configResponse;
|
||||
} else if (
|
||||
parameter.default !== undefined &&
|
||||
parameter.default !== null &&
|
||||
!configValues[parameter.name]
|
||||
) {
|
||||
// Fall back to default value if no config value exists
|
||||
newValues[parameter.name] = parameter.default;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load config for ${parameter.name}:`, error);
|
||||
// Fall back to default if read operation fails
|
||||
if (
|
||||
parameter.default !== undefined &&
|
||||
parameter.default !== null &&
|
||||
!configValues[parameter.name]
|
||||
) {
|
||||
newValues[parameter.name] = parameter.default;
|
||||
}
|
||||
if (configResponse) {
|
||||
// Use the value from the config provider
|
||||
newValues[parameter.name] = String(configResponse);
|
||||
} else if (
|
||||
parameter.default !== undefined &&
|
||||
parameter.default !== null &&
|
||||
!configValues[parameter.name]
|
||||
) {
|
||||
// Fall back to default value if no config value exists
|
||||
newValues[parameter.name] = String(parameter.default);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load config for ${parameter.name}:`, error);
|
||||
// Fall back to default if read operation fails
|
||||
if (
|
||||
parameter.default !== undefined &&
|
||||
parameter.default !== null &&
|
||||
!configValues[parameter.name]
|
||||
) {
|
||||
newValues[parameter.name] = String(parameter.default);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update state with loaded values
|
||||
setConfigValues((prev) => ({
|
||||
...prev,
|
||||
...newValues,
|
||||
}));
|
||||
setIsLoading(false);
|
||||
};
|
||||
// Update state with loaded values
|
||||
setConfigValues((prev) => ({
|
||||
...prev,
|
||||
...newValues,
|
||||
}));
|
||||
setIsLoading(false);
|
||||
}, [configValues, parameters, read, setConfigValues]);
|
||||
|
||||
loadConfigValues().then();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
loadConfigValues();
|
||||
}, [loadConfigValues]);
|
||||
|
||||
// Filter parameters to only show required ones
|
||||
const requiredParameters = useMemo(() => {
|
||||
@@ -77,7 +107,7 @@ export default function DefaultProviderSetupForm({
|
||||
}, [parameters]);
|
||||
|
||||
// Helper function to generate appropriate placeholder text
|
||||
const getPlaceholder = (parameter) => {
|
||||
const getPlaceholder = (parameter: ConfigParameter): string => {
|
||||
// If default is defined and not null, show it
|
||||
if (parameter.default !== undefined && parameter.default !== null) {
|
||||
return `Default: ${parameter.default}`;
|
||||
|
||||
+14
-11
@@ -2,8 +2,8 @@ import { PROVIDER_REGISTRY } from '../../../ProviderRegistry';
|
||||
import { Input } from '../../../../../ui/input';
|
||||
import React from 'react';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Lock, RefreshCw } from 'lucide-react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import CustomRadio from '../../../../../ui/CustomRadio';
|
||||
|
||||
export default function OllamaForm({ configValues, setConfigValues, provider }) {
|
||||
@@ -12,12 +12,15 @@ export default function OllamaForm({ configValues, setConfigValues, provider })
|
||||
const [isCheckingLocal, setIsCheckingLocal] = useState(false);
|
||||
const [isLocalAvailable, setIsLocalAvailable] = useState(false);
|
||||
|
||||
const handleConnectionTypeChange = (value) => {
|
||||
setConfigValues((prev) => ({
|
||||
...prev,
|
||||
connection_type: value,
|
||||
}));
|
||||
};
|
||||
const handleConnectionTypeChange = useCallback(
|
||||
(value) => {
|
||||
setConfigValues((prev) => ({
|
||||
...prev,
|
||||
connection_type: value,
|
||||
}));
|
||||
},
|
||||
[setConfigValues]
|
||||
);
|
||||
|
||||
// Function to handle input changes and auto-select/deselect the host radio
|
||||
const handleInputChange = (paramName, value) => {
|
||||
@@ -40,7 +43,7 @@ export default function OllamaForm({ configValues, setConfigValues, provider })
|
||||
}
|
||||
};
|
||||
|
||||
const checkLocalAvailability = async () => {
|
||||
const checkLocalAvailability = useCallback(async () => {
|
||||
setIsCheckingLocal(true);
|
||||
|
||||
// Dummy implementation - simulates checking local availability
|
||||
@@ -69,12 +72,12 @@ export default function OllamaForm({ configValues, setConfigValues, provider })
|
||||
} finally {
|
||||
setIsCheckingLocal(false);
|
||||
}
|
||||
};
|
||||
}, [configValues.connection_type, handleConnectionTypeChange]);
|
||||
|
||||
// Check local availability on initial load
|
||||
useEffect(() => {
|
||||
checkLocalAvailability();
|
||||
}, []);
|
||||
}, [checkLocalAvailability]);
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React from 'react';
|
||||
import CardActions from './CardActions';
|
||||
import ConfigurationAction from '../interfaces/ConfigurationAction';
|
||||
|
||||
interface CardBodyProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ProviderCard = memo(function ProviderCard({
|
||||
const providerMetadata: ProviderMetadata | null = provider?.metadata || null;
|
||||
|
||||
// Instead of useEffect for logging, use useMemo to memoize the metadata
|
||||
const metadata = useMemo(() => providerMetadata, [provider]);
|
||||
const metadata = useMemo(() => providerMetadata, [providerMetadata]);
|
||||
|
||||
if (!metadata) {
|
||||
return <div>ProviderCard error: No metadata provided</div>;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Button } from '../../../../ui/button';
|
||||
import clsx from 'clsx';
|
||||
import { TooltipWrapper } from './TooltipWrapper';
|
||||
import { Check, CircleHelp, Plus, RefreshCw, Rocket, Sliders, X } from 'lucide-react';
|
||||
import { Check, Rocket, Sliders } from 'lucide-react';
|
||||
|
||||
interface ActionButtonProps extends React.ComponentProps<typeof Button> {
|
||||
/** Icon component to render, e.g. `RefreshCw` from lucide-react */
|
||||
|
||||
@@ -6,13 +6,11 @@ export function BaseModal({
|
||||
title,
|
||||
children,
|
||||
actions,
|
||||
onClose,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
actions: React.ReactNode; // Buttons for actions
|
||||
onClose: () => void;
|
||||
}) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import React, { useMemo, useState, useEffect, useRef } from 'react';
|
||||
import { Buffer } from 'buffer';
|
||||
import Copy from '../icons/Copy';
|
||||
import Modal from '../Modal';
|
||||
import { Card } from '../ui/card';
|
||||
import { Card } from './card';
|
||||
|
||||
interface BotConfig {
|
||||
instructions?: string;
|
||||
activities?: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface DeepLinkModalProps {
|
||||
botConfig: any;
|
||||
botConfig: BotConfig;
|
||||
onClose: () => void;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
// Function to generate a deep link from a bot config
|
||||
export function generateDeepLink(botConfig: any): string {
|
||||
export function generateDeepLink(botConfig: BotConfig): string {
|
||||
const configBase64 = Buffer.from(JSON.stringify(botConfig)).toString('base64');
|
||||
return `goose://bot?config=${configBase64}`;
|
||||
}
|
||||
|
||||
export function DeepLinkModal({
|
||||
botConfig: initialBotConfig,
|
||||
onClose,
|
||||
onOpen,
|
||||
}: DeepLinkModalProps) {
|
||||
export function DeepLinkModal({ botConfig: initialBotConfig, onClose }: DeepLinkModalProps) {
|
||||
// Create editable state for the bot config
|
||||
const [botConfig, setBotConfig] = useState(initialBotConfig);
|
||||
const [instructions, setInstructions] = useState(initialBotConfig.instructions || '');
|
||||
@@ -61,7 +61,7 @@ export function DeepLinkModal({
|
||||
instructions,
|
||||
activities,
|
||||
});
|
||||
}, [instructions, activities]);
|
||||
}, [instructions, activities, botConfig]);
|
||||
|
||||
// Handle adding a new activity
|
||||
const handleAddActivity = () => {
|
||||
|
||||
Reference in New Issue
Block a user