Changed app settings configuration form to match settings panels (#3829)
This commit is contained in:
@@ -10,6 +10,7 @@ import { MainPanelLayout } from '../Layout/MainPanelLayout';
|
|||||||
import { Bot, Share2, Monitor, MessageSquare } from 'lucide-react';
|
import { Bot, Share2, Monitor, MessageSquare } from 'lucide-react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import ChatSettingsSection from './chat/ChatSettingsSection';
|
import ChatSettingsSection from './chat/ChatSettingsSection';
|
||||||
|
import { CONFIGURATION_ENABLED } from '../../updates';
|
||||||
|
|
||||||
export type SettingsViewOptions = {
|
export type SettingsViewOptions = {
|
||||||
deepLinkConfig?: ExtensionConfig;
|
deepLinkConfig?: ExtensionConfig;
|
||||||
@@ -126,7 +127,7 @@ export default function SettingsView({
|
|||||||
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
|
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
|
||||||
>
|
>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<ConfigSettings />
|
{CONFIGURATION_ENABLED && <ConfigSettings />}
|
||||||
<AppSettingsSection scrollToSection={viewOptions.section} />
|
<AppSettingsSection scrollToSection={viewOptions.section} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -475,7 +475,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
|||||||
className="h-8 w-auto"
|
className="h-8 w-auto"
|
||||||
/>
|
/>
|
||||||
<span className="text-2xl font-mono text-black dark:text-white">
|
<span className="text-2xl font-mono text-black dark:text-white">
|
||||||
{String(window.appConfig.get('GOOSE_VERSION') || 'Block Internal v2.1.0')}
|
{String(window.appConfig.get('GOOSE_VERSION') || 'Development')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1,22 +1,50 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { Input } from '../../ui/input';
|
import { Input } from '../../ui/input';
|
||||||
import { Button } from '../../ui/button';
|
import { Button } from '../../ui/button';
|
||||||
import { useConfig } from '../../ConfigContext';
|
import { useConfig } from '../../ConfigContext';
|
||||||
import { cn } from '../../../utils';
|
import { cn } from '../../../utils';
|
||||||
import { Save, RotateCcw, FileText } from 'lucide-react';
|
import { Save, RotateCcw, FileText, Settings } from 'lucide-react';
|
||||||
import { toastSuccess, toastError } from '../../../toasts';
|
import { toastSuccess, toastError } from '../../../toasts';
|
||||||
import { getUiNames, providerPrefixes } from '../../../utils/configUtils';
|
import { getUiNames, providerPrefixes } from '../../../utils/configUtils';
|
||||||
import type { ConfigData, ConfigValue } from '../../../types/config';
|
import type { ConfigData, ConfigValue } from '../../../types/config';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '../../ui/dialog';
|
||||||
|
|
||||||
export default function ConfigSettings() {
|
export default function ConfigSettings() {
|
||||||
const { config, upsert } = useConfig();
|
const { config, upsert } = useConfig();
|
||||||
const typedConfig = config as ConfigData;
|
const typedConfig = config as ConfigData;
|
||||||
const [configValues, setConfigValues] = useState<ConfigData>({});
|
const [configValues, setConfigValues] = useState<ConfigData>({});
|
||||||
const [modified, setModified] = useState(false);
|
const [modifiedKeys, setModifiedKeys] = useState<Set<string>>(new Set());
|
||||||
const [saving, setSaving] = useState<string | null>(null);
|
const [saving, setSaving] = useState<string | null>(null);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [originalKeyOrder, setOriginalKeyOrder] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setConfigValues(typedConfig);
|
setConfigValues(typedConfig);
|
||||||
|
setModifiedKeys(new Set());
|
||||||
|
|
||||||
|
// Capture the original key order only on first load or when new keys are added
|
||||||
|
const currentKeys = Object.keys(typedConfig);
|
||||||
|
setOriginalKeyOrder((prevOrder) => {
|
||||||
|
if (prevOrder.length === 0) {
|
||||||
|
// First load - capture the initial order
|
||||||
|
return currentKeys;
|
||||||
|
} else if (currentKeys.length > prevOrder.length) {
|
||||||
|
// New keys have been added - add them to the end while preserving existing order
|
||||||
|
const newKeys = currentKeys.filter((key) => !prevOrder.includes(key));
|
||||||
|
return [...prevOrder, ...newKeys];
|
||||||
|
}
|
||||||
|
// Don't reorder when keys are just updated/saved - preserve the original order
|
||||||
|
return prevOrder;
|
||||||
|
});
|
||||||
}, [typedConfig]);
|
}, [typedConfig]);
|
||||||
|
|
||||||
const handleChange = (key: string, value: string) => {
|
const handleChange = (key: string, value: string) => {
|
||||||
@@ -24,7 +52,16 @@ export default function ConfigSettings() {
|
|||||||
...prev,
|
...prev,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
}));
|
}));
|
||||||
setModified(true);
|
|
||||||
|
setModifiedKeys((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (value !== String(typedConfig[key] || '')) {
|
||||||
|
newSet.add(key);
|
||||||
|
} else {
|
||||||
|
newSet.delete(key);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async (key: string) => {
|
const handleSave = async (key: string) => {
|
||||||
@@ -35,7 +72,13 @@ export default function ConfigSettings() {
|
|||||||
title: 'Configuration Updated',
|
title: 'Configuration Updated',
|
||||||
msg: `Successfully saved "${getUiNames(key)}"`,
|
msg: `Successfully saved "${getUiNames(key)}"`,
|
||||||
});
|
});
|
||||||
setModified(false);
|
|
||||||
|
// Remove this key from modified keys since it's now saved
|
||||||
|
setModifiedKeys((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(key);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save config:', error);
|
console.error('Failed to save config:', error);
|
||||||
toastError({
|
toastError({
|
||||||
@@ -50,97 +93,132 @@ export default function ConfigSettings() {
|
|||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setConfigValues(typedConfig);
|
setConfigValues(typedConfig);
|
||||||
setModified(false);
|
setModifiedKeys(new Set());
|
||||||
toastSuccess({
|
toastSuccess({
|
||||||
title: 'Configuration Reset',
|
title: 'Configuration Reset',
|
||||||
msg: 'All changes have been reverted',
|
msg: 'All changes have been reverted',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleModalClose = (open: boolean) => {
|
||||||
|
if (!open && modifiedKeys.size > 0) {
|
||||||
|
// Reset any unsaved changes when closing the modal
|
||||||
|
setConfigValues(typedConfig);
|
||||||
|
setModifiedKeys(new Set());
|
||||||
|
}
|
||||||
|
setIsModalOpen(open);
|
||||||
|
};
|
||||||
|
|
||||||
const currentProvider = typedConfig.GOOSE_PROVIDER || '';
|
const currentProvider = typedConfig.GOOSE_PROVIDER || '';
|
||||||
|
|
||||||
const currentProviderPrefixes = providerPrefixes[currentProvider] || [];
|
const configEntries: [string, ConfigValue][] = useMemo(() => {
|
||||||
|
const currentProviderPrefixes = providerPrefixes[currentProvider] || [];
|
||||||
|
const allProviderPrefixes = Object.values(providerPrefixes).flat();
|
||||||
|
|
||||||
const allProviderPrefixes = Object.values(providerPrefixes).flat();
|
return originalKeyOrder
|
||||||
|
.filter((key) => {
|
||||||
|
// skip secrets
|
||||||
|
if (key === 'extensions' || key.includes('_KEY') || key.includes('_TOKEN')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const providerSpecificEntries: [string, ConfigValue][] = [];
|
// Only show provider-specific entries for the current provider
|
||||||
const generalEntries: [string, ConfigValue][] = [];
|
const providerSpecific = allProviderPrefixes.some((prefix: string) =>
|
||||||
|
key.startsWith(prefix)
|
||||||
|
);
|
||||||
|
if (providerSpecific) {
|
||||||
|
return currentProviderPrefixes.some((prefix: string) => key.startsWith(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
Object.entries(configValues).forEach(([key, value]) => {
|
return true;
|
||||||
// skip secrets
|
})
|
||||||
if (key === 'extensions' || key.includes('_KEY') || key.includes('_TOKEN')) {
|
.map((key) => [key, configValues[key]]);
|
||||||
return;
|
}, [originalKeyOrder, configValues, currentProvider]);
|
||||||
}
|
|
||||||
|
|
||||||
const providerSpecific = allProviderPrefixes.some((prefix: string) => key.startsWith(prefix));
|
|
||||||
|
|
||||||
if (providerSpecific) {
|
|
||||||
if (currentProviderPrefixes.some((prefix: string) => key.startsWith(prefix))) {
|
|
||||||
providerSpecificEntries.push([key, value]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
generalEntries.push([key, value]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const configEntries = [...providerSpecificEntries, ...generalEntries];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="configEditor" className="px-8">
|
<Card className="rounded-lg">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<CardHeader className="pb-0">
|
||||||
<div className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<FileText className="text-iconStandard" size={20} />
|
<FileText className="text-iconStandard" size={20} />
|
||||||
<h2 className="text-xl font-medium text-textStandard">Configuration</h2>
|
Configuration
|
||||||
</div>
|
</CardTitle>
|
||||||
{modified && (
|
<CardDescription>
|
||||||
<Button onClick={handleReset} variant="ghost" className="text-sm">
|
Edit your goose configuration settings
|
||||||
<RotateCcw className="h-4 w-4 mr-2" />
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="pb-8">
|
|
||||||
<p className="text-sm text-textSubtle mb-6">
|
|
||||||
Edit your goose config
|
|
||||||
{currentProvider && ` (current settings for ${currentProvider})`}
|
{currentProvider && ` (current settings for ${currentProvider})`}
|
||||||
</p>
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-4 px-4">
|
||||||
|
<Dialog open={isModalOpen} onOpenChange={handleModalClose}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button className="flex items-center gap-2" variant="secondary" size="sm">
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
Edit Configuration
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[80vh]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<FileText className="text-iconStandard" size={20} />
|
||||||
|
Configuration Editor
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Edit your goose configuration settings
|
||||||
|
{currentProvider && ` (current settings for ${currentProvider})`}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="flex-1 max-h-[60vh] overflow-auto pr-4">
|
||||||
{configEntries.length === 0 ? (
|
<div className="space-y-4">
|
||||||
<p className="text-textSubtle">No configuration settings found.</p>
|
{configEntries.length === 0 ? (
|
||||||
) : (
|
<p className="text-textSubtle">No configuration settings found.</p>
|
||||||
configEntries.map(([key, _value]) => (
|
) : (
|
||||||
<div key={key} className="grid grid-cols-[200px_1fr_auto] gap-3 items-center">
|
configEntries.map(([key, _value]) => (
|
||||||
<label className="text-sm font-medium text-textStandard" title={key}>
|
<div key={key} className="grid grid-cols-[200px_1fr_auto] gap-3 items-center">
|
||||||
{getUiNames(key)}
|
<label className="text-sm font-medium text-textStandard" title={key}>
|
||||||
</label>
|
{getUiNames(key)}
|
||||||
<Input
|
</label>
|
||||||
value={String(configValues[key] || '')}
|
<Input
|
||||||
onChange={(e) => handleChange(key, e.target.value)}
|
value={String(configValues[key] || '')}
|
||||||
className={cn(
|
onChange={(e) => handleChange(key, e.target.value)}
|
||||||
'text-textStandard border-borderSubtle hover:border-borderStandard',
|
className={cn(
|
||||||
configValues[key] !== typedConfig[key] && 'border-blue-500'
|
'text-textStandard border-borderSubtle hover:border-borderStandard transition-colors',
|
||||||
)}
|
modifiedKeys.has(key) && 'border-blue-500 focus:ring-blue-500/20'
|
||||||
placeholder={`Enter ${getUiNames(key).toLowerCase()}`}
|
)}
|
||||||
/>
|
placeholder={`Enter ${getUiNames(key)}`}
|
||||||
<Button
|
/>
|
||||||
onClick={() => handleSave(key)}
|
<Button
|
||||||
disabled={configValues[key] === typedConfig[key] || saving === key}
|
onClick={() => handleSave(key)}
|
||||||
variant="ghost"
|
disabled={!modifiedKeys.has(key) || saving === key}
|
||||||
size="sm"
|
variant="ghost"
|
||||||
className="min-w-[60px]"
|
size="sm"
|
||||||
>
|
className="min-w-[60px]"
|
||||||
{saving === key ? (
|
>
|
||||||
<span className="text-xs">Saving...</span>
|
{saving === key ? (
|
||||||
) : (
|
<span className="text-xs">Saving...</span>
|
||||||
<Save className="h-4 w-4" />
|
) : (
|
||||||
)}
|
<Save className="h-4 w-4" />
|
||||||
</Button>
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
<DialogFooter className="gap-2">
|
||||||
</div>
|
{modifiedKeys.size > 0 && (
|
||||||
</section>
|
<Button onClick={handleReset} variant="outline">
|
||||||
|
<RotateCcw className="h-4 w-4 mr-2" />
|
||||||
|
Reset Changes
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button onClick={() => setIsModalOpen(false)} variant="default">
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export const UPDATES_ENABLED = true;
|
export const UPDATES_ENABLED = true;
|
||||||
export const COST_TRACKING_ENABLED = true;
|
export const COST_TRACKING_ENABLED = true;
|
||||||
export const ANNOUNCEMENTS_ENABLED = false;
|
export const ANNOUNCEMENTS_ENABLED = false;
|
||||||
|
export const CONFIGURATION_ENABLED = true;
|
||||||
|
|||||||
@@ -1,56 +1,56 @@
|
|||||||
export const configLabels: Record<string, string> = {
|
export const configLabels: Record<string, string> = {
|
||||||
// goose settings
|
// goose settings
|
||||||
GOOSE_PROVIDER: 'GOOSE_PROVIDER',
|
GOOSE_PROVIDER: 'Provider',
|
||||||
GOOSE_MODEL: 'GOOSE_MODEL',
|
GOOSE_MODEL: 'Model',
|
||||||
GOOSE_TEMPERATURE: 'GOOSE_TEMPERATURE',
|
GOOSE_TEMPERATURE: 'Temperature',
|
||||||
GOOSE_MODE: 'GOOSE_MODE',
|
GOOSE_MODE: 'Mode',
|
||||||
GOOSE_LEAD_PROVIDER: 'GOOSE_LEAD_PROVIDER',
|
GOOSE_LEAD_PROVIDER: 'Lead Provider',
|
||||||
GOOSE_LEAD_MODEL: 'GOOSE_LEAD_MODEL',
|
GOOSE_LEAD_MODEL: 'Lead Model',
|
||||||
GOOSE_PLANNER_PROVIDER: 'GOOSE_PLANNER_PROVIDER',
|
GOOSE_PLANNER_PROVIDER: 'Planner Provider',
|
||||||
GOOSE_PLANNER_MODEL: 'GOOSE_PLANNER_MODEL',
|
GOOSE_PLANNER_MODEL: 'Planner Model',
|
||||||
GOOSE_TOOLSHIM: 'GOOSE_TOOLSHIM',
|
GOOSE_TOOLSHIM: 'Tool Shim',
|
||||||
GOOSE_TOOLSHIM_OLLAMA_MODEL: 'GOOSE_TOOLSHIM_OLLAMA_MODEL',
|
GOOSE_TOOLSHIM_OLLAMA_MODEL: 'Tool Shim Ollama Model',
|
||||||
GOOSE_CLI_MIN_PRIORITY: 'GOOSE_CLI_MIN_PRIORITY',
|
GOOSE_CLI_MIN_PRIORITY: 'CLI Min Priority',
|
||||||
GOOSE_ALLOWLIST: 'GOOSE_ALLOWLIST',
|
GOOSE_ALLOWLIST: 'Allow List',
|
||||||
GOOSE_RECIPE_GITHUB_REPO: 'GOOSE_RECIPE_GITHUB_REPO',
|
GOOSE_RECIPE_GITHUB_REPO: 'Recipe GitHub Repo',
|
||||||
|
|
||||||
// openai
|
// openai
|
||||||
OPENAI_API_KEY: 'OPENAI_API_KEY',
|
OPENAI_API_KEY: 'OpenAI API Key',
|
||||||
OPENAI_HOST: 'OPENAI_HOST',
|
OPENAI_HOST: 'OpenAI Host',
|
||||||
OPENAI_BASE_PATH: 'OPENAI_BASE_PATH',
|
OPENAI_BASE_PATH: 'OpenAI Base Path',
|
||||||
|
|
||||||
// groq
|
// groq
|
||||||
GROQ_API_KEY: 'GROQ_API_KEY',
|
GROQ_API_KEY: 'Groq API Key',
|
||||||
|
|
||||||
// openrouter
|
// openrouter
|
||||||
OPENROUTER_API_KEY: 'OPENROUTER_API_KEY',
|
OPENROUTER_API_KEY: 'OpenRouter API Key',
|
||||||
|
|
||||||
// anthropic
|
// anthropic
|
||||||
ANTHROPIC_API_KEY: 'ANTHROPIC_API_KEY',
|
ANTHROPIC_API_KEY: 'Anthropic API Key',
|
||||||
ANTHROPIC_HOST: 'ANTHROPIC_HOST',
|
ANTHROPIC_HOST: 'Anthropic Host',
|
||||||
|
|
||||||
// google
|
// google
|
||||||
GOOGLE_API_KEY: 'GOOGLE_API_KEY',
|
GOOGLE_API_KEY: 'Google API Key',
|
||||||
|
|
||||||
// databricks
|
// databricks
|
||||||
DATABRICKS_HOST: 'DATABRICKS_HOST',
|
DATABRICKS_HOST: 'Databricks Host',
|
||||||
|
|
||||||
// ollama
|
// ollama
|
||||||
OLLAMA_HOST: 'OLLAMA_HOST',
|
OLLAMA_HOST: 'Ollama Host',
|
||||||
|
|
||||||
// azure openai
|
// azure openai
|
||||||
AZURE_OPENAI_API_KEY: 'AZURE_OPENAI_API_KEY',
|
AZURE_OPENAI_API_KEY: 'Azure OpenAI API Key',
|
||||||
AZURE_OPENAI_ENDPOINT: 'AZURE_OPENAI_ENDPOINT',
|
AZURE_OPENAI_ENDPOINT: 'Azure OpenAI Endpoint',
|
||||||
AZURE_OPENAI_DEPLOYMENT_NAME: 'AZURE_OPENAI_DEPLOYMENT_NAME',
|
AZURE_OPENAI_DEPLOYMENT_NAME: 'Azure OpenAI Deployment Name',
|
||||||
AZURE_OPENAI_API_VERSION: 'AZURE_OPENAI_API_VERSION',
|
AZURE_OPENAI_API_VERSION: 'Azure OpenAI API Version',
|
||||||
|
|
||||||
// gcp vertex
|
// gcp vertex
|
||||||
GCP_PROJECT_ID: 'GCP_PROJECT_ID',
|
GCP_PROJECT_ID: 'GCP Project ID',
|
||||||
GCP_LOCATION: 'GCP_LOCATION',
|
GCP_LOCATION: 'GCP Location',
|
||||||
|
|
||||||
// snowflake
|
// snowflake
|
||||||
SNOWFLAKE_HOST: 'SNOWFLAKE_HOST',
|
SNOWFLAKE_HOST: 'Snowflake Host',
|
||||||
SNOWFLAKE_TOKEN: 'SNOWFLAKE_TOKEN',
|
SNOWFLAKE_TOKEN: 'Snowflake Token',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const providerPrefixes: Record<string, string[]> = {
|
export const providerPrefixes: Record<string, string[]> = {
|
||||||
|
|||||||
Reference in New Issue
Block a user