mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Lifei/clean up old onboarding flow (#8099)
This commit is contained in:
@@ -387,7 +387,6 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::status::diagnostics,
|
||||
super::routes::mcp_ui_proxy::mcp_ui_proxy,
|
||||
super::routes::config_management::backup_config,
|
||||
super::routes::config_management::detect_provider,
|
||||
super::routes::config_management::recover_config,
|
||||
super::routes::config_management::validate_config,
|
||||
super::routes::config_management::init_config,
|
||||
@@ -484,8 +483,6 @@ derive_utoipa!(Icon as IconSchema);
|
||||
components(schemas(
|
||||
super::routes::config_management::UpsertConfigQuery,
|
||||
super::routes::config_management::ConfigKeyQuery,
|
||||
super::routes::config_management::DetectProviderRequest,
|
||||
super::routes::config_management::DetectProviderResponse,
|
||||
super::routes::config_management::ConfigResponse,
|
||||
super::routes::config_management::ProvidersResponse,
|
||||
super::routes::config_management::ProviderDetails,
|
||||
|
||||
@@ -12,7 +12,6 @@ use goose::config::paths::Paths;
|
||||
use goose::config::ExtensionEntry;
|
||||
use goose::config::{Config, ConfigError};
|
||||
use goose::model::ModelConfig;
|
||||
use goose::providers::auto_detect::detect_provider_from_api_key;
|
||||
use goose::providers::base::{ProviderMetadata, ProviderType};
|
||||
use goose::providers::canonical::maybe_get_canonical_model;
|
||||
use goose::providers::catalog::{
|
||||
@@ -149,16 +148,6 @@ pub struct SlashCommandsResponse {
|
||||
pub commands: Vec<SlashCommand>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct DetectProviderRequest {
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct DetectProviderResponse {
|
||||
pub provider_name: String,
|
||||
pub models: Vec<String>,
|
||||
}
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/config/upsert",
|
||||
@@ -534,31 +523,6 @@ pub async fn upsert_permissions(
|
||||
Ok(Json("Permissions updated successfully".to_string()))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/config/detect-provider",
|
||||
request_body = DetectProviderRequest,
|
||||
responses(
|
||||
(status = 200, description = "Provider detected successfully", body = DetectProviderResponse),
|
||||
(status = 404, description = "No matching provider found"),
|
||||
)
|
||||
)]
|
||||
pub async fn detect_provider(
|
||||
Json(detect_request): Json<DetectProviderRequest>,
|
||||
) -> Result<Json<DetectProviderResponse>, ErrorResponse> {
|
||||
let api_key = detect_request.api_key.trim();
|
||||
|
||||
match detect_provider_from_api_key(api_key).await {
|
||||
Some((provider_name, models)) => Ok(Json(DetectProviderResponse {
|
||||
provider_name,
|
||||
models,
|
||||
})),
|
||||
None => Err(ErrorResponse::not_found(
|
||||
"Could not detect provider from the provided API key",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/config/backup",
|
||||
@@ -930,7 +894,6 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
"/config/providers/{name}/cleanup",
|
||||
post(cleanup_provider_cache),
|
||||
)
|
||||
.route("/config/detect-provider", post(detect_provider))
|
||||
.route("/config/slash_commands", get(get_slash_commands))
|
||||
.route(
|
||||
"/config/canonical-model-info",
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
use crate::model::ModelConfig;
|
||||
use crate::providers::retry::{retry_operation, RetryConfig};
|
||||
|
||||
pub async fn detect_provider_from_api_key(api_key: &str) -> Option<(String, Vec<String>)> {
|
||||
let provider_tests = vec![
|
||||
("anthropic", "ANTHROPIC_API_KEY"),
|
||||
("openai", "OPENAI_API_KEY"),
|
||||
("google", "GOOGLE_API_KEY"),
|
||||
("groq", "GROQ_API_KEY"),
|
||||
("xai", "XAI_API_KEY"),
|
||||
// Ollama and OpenRouter don't validate keys, so they would match any input
|
||||
];
|
||||
|
||||
let tasks: Vec<_> = provider_tests
|
||||
.into_iter()
|
||||
.map(|(provider_name, env_key)| {
|
||||
let api_key = api_key.to_string();
|
||||
tokio::spawn(async move {
|
||||
let original_value = std::env::var(env_key).ok();
|
||||
std::env::set_var(env_key, &api_key);
|
||||
|
||||
let result = match crate::providers::create(
|
||||
provider_name,
|
||||
ModelConfig::new_or_fail("default").with_canonical_limits(provider_name),
|
||||
Vec::new(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(provider) => {
|
||||
match retry_operation(&RetryConfig::default(), || async {
|
||||
provider.fetch_supported_models().await
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(models) if !models.is_empty() => {
|
||||
Some((provider_name.to_string(), models))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
match original_value {
|
||||
Some(val) => std::env::set_var(env_key, val),
|
||||
None => std::env::remove_var(env_key),
|
||||
}
|
||||
|
||||
result
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for task in tasks {
|
||||
if let Ok(Some(result)) = task.await {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
pub mod anthropic;
|
||||
pub mod api_client;
|
||||
pub mod auto_detect;
|
||||
pub mod avian;
|
||||
pub mod azure;
|
||||
pub mod azureauth;
|
||||
|
||||
@@ -969,39 +969,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/detect-provider": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "detect_provider",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DetectProviderRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Provider detected successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DetectProviderResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No matching provider found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/extensions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -4701,35 +4668,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DetectProviderRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"api_key"
|
||||
],
|
||||
"properties": {
|
||||
"api_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DetectProviderResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"provider_name",
|
||||
"models"
|
||||
],
|
||||
"properties": {
|
||||
"models": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"provider_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DictationProvider": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -61,10 +61,6 @@ vi.mock('./sessions', () => ({
|
||||
generateSessionId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./utils/openRouterSetup', () => ({
|
||||
startOpenRouterSetup: vi.fn().mockResolvedValue({ success: false, message: 'Test' }),
|
||||
}));
|
||||
|
||||
// Mock the ConfigContext module
|
||||
vi.mock('./components/ConfigContext', () => ({
|
||||
useConfig: () => ({
|
||||
@@ -83,19 +79,6 @@ vi.mock('./components/ErrorBoundary', () => ({
|
||||
ErrorUI: ({ error }: { error: Error }) => <div>Error: {error.message}</div>,
|
||||
}));
|
||||
|
||||
// Mock ProviderGuard to show the welcome screen when no provider is configured
|
||||
vi.mock('./components/ProviderGuard', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => {
|
||||
// In a real app, ProviderGuard would check for provider and show welcome screen
|
||||
// For this test, we'll simulate that behavior
|
||||
const hasProvider = window.electron?.getConfig()?.GOOSE_DEFAULT_PROVIDER;
|
||||
if (!hasProvider) {
|
||||
return <div>Welcome to Goose!</div>;
|
||||
}
|
||||
return <>{children}</>;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('./components/ModelAndProviderContext', () => ({
|
||||
ModelAndProviderProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
useModelAndProvider: () => ({
|
||||
|
||||
+7
-26
@@ -15,9 +15,7 @@ import { ExtensionInstallModal } from './components/ExtensionInstallModal';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import AnnouncementModal from './components/AnnouncementModal';
|
||||
import TelemetryOptOutModal from './components/TelemetryOptOutModal';
|
||||
import ProviderGuard from './components/ProviderGuard';
|
||||
import OnboardingGuard from './components/onboarding/OnboardingGuard';
|
||||
import { USE_NEW_ONBOARDING } from './featureFlags';
|
||||
import { createSession } from './sessions';
|
||||
|
||||
import { ChatType } from './types/chat';
|
||||
@@ -249,11 +247,7 @@ const ConfigureProvidersRoute = () => {
|
||||
);
|
||||
};
|
||||
|
||||
interface WelcomeRouteProps {
|
||||
onSelectProvider: () => void;
|
||||
}
|
||||
|
||||
const WelcomeRoute = ({ onSelectProvider }: WelcomeRouteProps) => {
|
||||
const WelcomeRoute = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
@@ -265,7 +259,6 @@ const WelcomeRoute = ({ onSelectProvider }: WelcomeRouteProps) => {
|
||||
isOnboarding={true}
|
||||
onProviderLaunched={(model?: string) => {
|
||||
trackOnboardingCompleted('other', model);
|
||||
onSelectProvider();
|
||||
navigate('/', { replace: true });
|
||||
}}
|
||||
/>
|
||||
@@ -351,7 +344,6 @@ export function AppInner() {
|
||||
const [fatalError, setFatalError] = useState<string | null>(null);
|
||||
const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false);
|
||||
const [sharedSessionError, setSharedSessionError] = useState<string | null>(null);
|
||||
const [didSelectProvider, setDidSelectProvider] = useState<boolean>(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const setView = useNavigation();
|
||||
@@ -650,28 +642,17 @@ export function AppInner() {
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
<Routes>
|
||||
<Route path="launcher" element={<LauncherView />} />
|
||||
<Route
|
||||
path="welcome"
|
||||
element={<WelcomeRoute onSelectProvider={() => setDidSelectProvider(true)} />}
|
||||
/>
|
||||
<Route path="welcome" element={<WelcomeRoute />} />
|
||||
<Route path="configure-providers" element={<ConfigureProvidersRoute />} />
|
||||
<Route path="standalone-app" element={<StandaloneAppView />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
USE_NEW_ONBOARDING ? (
|
||||
<OnboardingGuard>
|
||||
<ChatProvider chat={chat} setChat={setChat} contextKey="hub">
|
||||
<AppLayout activeSessions={activeSessions} />
|
||||
</ChatProvider>
|
||||
</OnboardingGuard>
|
||||
) : (
|
||||
<ProviderGuard didSelectProvider={didSelectProvider}>
|
||||
<ChatProvider chat={chat} setChat={setChat} contextKey="hub">
|
||||
<AppLayout activeSessions={activeSessions} />
|
||||
</ChatProvider>
|
||||
</ProviderGuard>
|
||||
)
|
||||
<OnboardingGuard>
|
||||
<ChatProvider chat={chat} setChat={setChat} contextKey="hub">
|
||||
<AppLayout activeSessions={activeSessions} />
|
||||
</ChatProvider>
|
||||
</OnboardingGuard>
|
||||
}
|
||||
>
|
||||
<Route index element={<HubRouteWrapper />} />
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -234,15 +234,6 @@ export type DeleteRecipeRequest = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type DetectProviderRequest = {
|
||||
api_key: string;
|
||||
};
|
||||
|
||||
export type DetectProviderResponse = {
|
||||
models: Array<string>;
|
||||
provider_name: string;
|
||||
};
|
||||
|
||||
export type DictationProvider = 'openai' | 'elevenlabs' | 'groq' | 'local';
|
||||
|
||||
export type DictationProviderStatus = {
|
||||
@@ -2356,29 +2347,6 @@ export type UpdateCustomProviderResponses = {
|
||||
|
||||
export type UpdateCustomProviderResponse = UpdateCustomProviderResponses[keyof UpdateCustomProviderResponses];
|
||||
|
||||
export type DetectProviderData = {
|
||||
body: DetectProviderRequest;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/config/detect-provider';
|
||||
};
|
||||
|
||||
export type DetectProviderErrors = {
|
||||
/**
|
||||
* No matching provider found
|
||||
*/
|
||||
404: unknown;
|
||||
};
|
||||
|
||||
export type DetectProviderResponses = {
|
||||
/**
|
||||
* Provider detected successfully
|
||||
*/
|
||||
200: DetectProviderResponse;
|
||||
};
|
||||
|
||||
export type DetectProviderResponse2 = DetectProviderResponses[keyof DetectProviderResponses];
|
||||
|
||||
export type GetExtensionsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { detectProvider } from '../api';
|
||||
import { Key } from './icons/Key';
|
||||
import { ArrowRight } from './icons/ArrowRight';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface ApiKeyTesterProps {
|
||||
onSuccess: (provider: string, model: string, apiKey: string) => void;
|
||||
onStartTesting?: () => void;
|
||||
}
|
||||
|
||||
interface DetectionResult {
|
||||
provider: string;
|
||||
model: string;
|
||||
totalModels: number;
|
||||
}
|
||||
|
||||
export default function ApiKeyTester({ onSuccess, onStartTesting }: ApiKeyTesterProps) {
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [result, setResult] = useState<DetectionResult | null>(null);
|
||||
const [error, setError] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const testApiKey = async () => {
|
||||
const actualValue = inputRef.current?.value || apiKey;
|
||||
|
||||
if (!actualValue.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
onStartTesting?.();
|
||||
|
||||
setIsLoading(true);
|
||||
setResult(null);
|
||||
setError(false);
|
||||
|
||||
try {
|
||||
const response = await detectProvider({
|
||||
body: { api_key: actualValue },
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
const { provider_name, models } = response.data;
|
||||
|
||||
setResult({
|
||||
provider: provider_name,
|
||||
model: models[0],
|
||||
totalModels: models.length,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
onSuccess(provider_name, models[0], actualValue);
|
||||
}, 1500);
|
||||
}
|
||||
} catch {
|
||||
setError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hasInput = apiKey.trim().length > 0;
|
||||
const canSubmit = hasInput && !isLoading;
|
||||
|
||||
return (
|
||||
<div className="relative w-full mb-6">
|
||||
{/* Recommended pill */}
|
||||
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-blue-600 text-white rounded-full">
|
||||
Recommended if you have API access already
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full p-3 sm:p-4 bg-background-secondary border rounded-xl">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Key className="w-4 h-4 text-text-primary flex-shrink-0" />
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2">
|
||||
<h3 className="font-medium text-text-primary text-sm sm:text-base">
|
||||
Quick Setup with API Key
|
||||
</h3>
|
||||
<span className="text-text-secondary text-xs sm:text-sm">
|
||||
Auto-detect your provider
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2 items-stretch">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="Enter your API key (OpenAI, Anthropic, Google, etc.)"
|
||||
className="flex-1 px-3 py-2 border rounded-lg bg-background-primary text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
disabled={isLoading}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && canSubmit) {
|
||||
testApiKey();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={testApiKey}
|
||||
disabled={!canSubmit}
|
||||
variant={canSubmit ? 'default' : 'secondary'}
|
||||
className="h-auto py-2 px-4"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"></div>
|
||||
) : (
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Loading state */}
|
||||
{isLoading && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-background-secondary rounded text-sm text-text-secondary">
|
||||
<div className="w-3 h-3 border-2 border-current border-t-transparent rounded-full animate-spin"></div>
|
||||
<span>Detecting provider and validating key...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success result */}
|
||||
{result && (
|
||||
<div className="flex items-center gap-2 text-sm p-3 rounded-lg bg-green-50 text-green-800 border border-green-200 dark:bg-green-900/20 dark:text-green-200 dark:border-green-800">
|
||||
<span>✅</span>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">Detected {result.provider}</div>
|
||||
<div className="text-green-600 dark:text-green-400 text-xs mt-1">
|
||||
Model: {result.model} ({result.totalModels} models available)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error result */}
|
||||
{error && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm p-3 rounded-lg bg-red-50 text-red-800 border border-red-200 dark:bg-red-900/20 dark:text-red-200 dark:border-red-800">
|
||||
<span>❌</span>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">Provider Detection Failed</div>
|
||||
<div className="text-red-600 dark:text-red-400 text-xs mt-1">
|
||||
Could not detect provider from API key
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-6 space-y-1">
|
||||
<p className="text-xs font-medium text-text-secondary">Suggestions:</p>
|
||||
<ul className="text-xs text-text-secondary space-y-1">
|
||||
<li className="flex items-start gap-1">
|
||||
<span className="text-blue-500 mt-0.5">•</span>
|
||||
<span>
|
||||
Make sure you are using a valid API key from OpenAI, Anthropic, Google, Groq,
|
||||
or xAI
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-1">
|
||||
<span className="text-blue-500 mt-0.5">•</span>
|
||||
<span>Check that the key is complete and not truncated</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-1">
|
||||
<span className="text-blue-500 mt-0.5">•</span>
|
||||
<span>Verify your API key is active and has sufficient credits</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-1">
|
||||
<span className="text-blue-500 mt-0.5">•</span>
|
||||
<span>For local Ollama setup, use the "Other Providers" section below</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useConfig } from './ConfigContext';
|
||||
import { toastService } from '../toasts';
|
||||
import { Goose } from './icons';
|
||||
import LocalModelPicker from './onboarding/LocalModelPicker';
|
||||
|
||||
interface LocalModelSetupProps {
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function LocalModelSetup({ onSuccess, onCancel }: LocalModelSetupProps) {
|
||||
const { upsert } = useConfig();
|
||||
|
||||
const handleConfigured = async (_providerName: string, modelId: string) => {
|
||||
await upsert('GOOSE_PROVIDER', 'local', false);
|
||||
await upsert('GOOSE_MODEL', modelId, false);
|
||||
toastService.success({
|
||||
title: 'Local Model Ready',
|
||||
msg: `Running entirely on your machine with ${modelId}.`,
|
||||
});
|
||||
onSuccess();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-left space-y-3">
|
||||
<div className="origin-bottom-left goose-icon-animation">
|
||||
<Goose className="size-6 sm:size-8" />
|
||||
</div>
|
||||
<h1 className="text-2xl sm:text-4xl font-light">Run Locally</h1>
|
||||
<p className="text-text-muted text-base sm:text-lg">
|
||||
Download a model to run goose entirely on your machine — no API keys, no accounts,
|
||||
completely free and private.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<LocalModelPicker onConfigured={handleConfigured} onBack={onCancel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,582 +0,0 @@
|
||||
import { useEffect, useState, useMemo, useRef, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useConfig } from './ConfigContext';
|
||||
import { SetupModal } from './SetupModal';
|
||||
import { startOpenRouterSetup } from '../utils/openRouterSetup';
|
||||
import { startTetrateSetup } from '../utils/tetrateSetup';
|
||||
import { startChatGptCodexSetup } from '../utils/chatgptCodexSetup';
|
||||
import WelcomeGooseLogo from './WelcomeGooseLogo';
|
||||
import { toastService } from '../toasts';
|
||||
import { LocalModelSetup } from './LocalModelSetup';
|
||||
import ApiKeyTester from './ApiKeyTester';
|
||||
import { SwitchModelModal } from './settings/models/subcomponents/SwitchModelModal';
|
||||
import { createNavigationHandler } from '../utils/navigationUtils';
|
||||
import TelemetrySettings from './settings/app/TelemetrySettings';
|
||||
import {
|
||||
trackOnboardingStarted,
|
||||
trackOnboardingProviderSelected,
|
||||
trackOnboardingCompleted,
|
||||
trackOnboardingAbandoned,
|
||||
trackOnboardingSetupFailed,
|
||||
} from '../utils/analytics';
|
||||
|
||||
import { Goose, OpenRouter, Tetrate, ChatGPT } from './icons';
|
||||
|
||||
interface ProviderGuardProps {
|
||||
didSelectProvider: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ProviderGuard({ didSelectProvider, children }: ProviderGuardProps) {
|
||||
const { read, upsert, getProviders } = useConfig();
|
||||
const navigate = useNavigate();
|
||||
const [isChecking, setIsChecking] = useState(true);
|
||||
const [hasProvider, setHasProvider] = useState(false);
|
||||
const [showFirstTimeSetup, setShowFirstTimeSetup] = useState(false);
|
||||
const [showLocalModelSetup, setShowLocalModelSetup] = useState(false);
|
||||
const [userInActiveSetup, setUserInActiveSetup] = useState(false);
|
||||
const [showSwitchModelModal, setShowSwitchModelModal] = useState(false);
|
||||
const [switchModelProvider, setSwitchModelProvider] = useState<string | null>(null);
|
||||
const onboardingTracked = useRef(false);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [showScrollIndicator, setShowScrollIndicator] = useState(true);
|
||||
|
||||
const checkScrollPosition = useCallback(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = container;
|
||||
const isNearBottom = scrollTop + clientHeight >= scrollHeight - 50;
|
||||
const canScroll = scrollHeight > clientHeight;
|
||||
|
||||
setShowScrollIndicator(canScroll && !isNearBottom);
|
||||
}, []);
|
||||
|
||||
const setView = useMemo(() => createNavigationHandler(navigate), [navigate]);
|
||||
|
||||
const [openRouterSetupState, setOpenRouterSetupState] = useState<{
|
||||
show: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
showRetry: boolean;
|
||||
autoClose?: number;
|
||||
} | null>(null);
|
||||
|
||||
const [tetrateSetupState, setTetrateSetupState] = useState<{
|
||||
show: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
showRetry: boolean;
|
||||
autoClose?: number;
|
||||
} | null>(null);
|
||||
|
||||
const [chatgptCodexSetupState, setChatgptCodexSetupState] = useState<{
|
||||
show: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
showRetry: boolean;
|
||||
autoClose?: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleTetrateSetup = async () => {
|
||||
trackOnboardingProviderSelected({ method: 'tetrate' });
|
||||
try {
|
||||
const result = await startTetrateSetup();
|
||||
if (result.success) {
|
||||
setSwitchModelProvider('tetrate');
|
||||
setShowSwitchModelModal(true);
|
||||
} else {
|
||||
trackOnboardingSetupFailed('tetrate', result.message);
|
||||
setTetrateSetupState({
|
||||
show: true,
|
||||
title: 'Setup Failed',
|
||||
message: result.message,
|
||||
showRetry: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Tetrate setup error:', error);
|
||||
trackOnboardingSetupFailed('tetrate', 'unexpected_error');
|
||||
setTetrateSetupState({
|
||||
show: true,
|
||||
title: 'Setup Error',
|
||||
message: 'An unexpected error occurred during setup.',
|
||||
showRetry: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleChatGptCodexSetup = async () => {
|
||||
trackOnboardingProviderSelected({ method: 'chatgpt_codex' });
|
||||
try {
|
||||
const result = await startChatGptCodexSetup();
|
||||
if (result.success) {
|
||||
await getProviders(true);
|
||||
setSwitchModelProvider('chatgpt_codex');
|
||||
setShowSwitchModelModal(true);
|
||||
} else {
|
||||
trackOnboardingSetupFailed('chatgpt_codex', result.message);
|
||||
setChatgptCodexSetupState({
|
||||
show: true,
|
||||
title: 'Setup Failed',
|
||||
message: result.message,
|
||||
showRetry: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ChatGPT Codex setup error:', error);
|
||||
trackOnboardingSetupFailed('chatgpt_codex', 'unexpected_error');
|
||||
setChatgptCodexSetupState({
|
||||
show: true,
|
||||
title: 'Setup Error',
|
||||
message: 'An unexpected error occurred during setup.',
|
||||
showRetry: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleApiKeySuccess = async (provider: string, _model: string, apiKey: string) => {
|
||||
trackOnboardingProviderSelected({ method: 'api_key' });
|
||||
const keyName = `${provider.toUpperCase()}_API_KEY`;
|
||||
await upsert(keyName, apiKey, true);
|
||||
await upsert('GOOSE_PROVIDER', provider, false);
|
||||
|
||||
setSwitchModelProvider(provider);
|
||||
setShowSwitchModelModal(true);
|
||||
};
|
||||
|
||||
const handleModelSelected = (model: string) => {
|
||||
if (switchModelProvider) {
|
||||
trackOnboardingCompleted(switchModelProvider, model);
|
||||
}
|
||||
setShowSwitchModelModal(false);
|
||||
setUserInActiveSetup(false);
|
||||
setShowFirstTimeSetup(false);
|
||||
setHasProvider(true);
|
||||
navigate('/', { replace: true });
|
||||
};
|
||||
|
||||
const handleSwitchModelClose = () => {
|
||||
setShowSwitchModelModal(false);
|
||||
};
|
||||
|
||||
const handleOpenRouterSetup = async () => {
|
||||
trackOnboardingProviderSelected({ method: 'openrouter' });
|
||||
try {
|
||||
const result = await startOpenRouterSetup();
|
||||
if (result.success) {
|
||||
setSwitchModelProvider('openrouter');
|
||||
setShowSwitchModelModal(true);
|
||||
} else {
|
||||
trackOnboardingSetupFailed('openrouter', result.message);
|
||||
setOpenRouterSetupState({
|
||||
show: true,
|
||||
title: 'Setup Failed',
|
||||
message: result.message,
|
||||
showRetry: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('OpenRouter setup error:', error);
|
||||
trackOnboardingSetupFailed('openrouter', 'unexpected_error');
|
||||
setOpenRouterSetupState({
|
||||
show: true,
|
||||
title: 'Setup Error',
|
||||
message: 'An unexpected error occurred during setup.',
|
||||
showRetry: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleLocalModelComplete = () => {
|
||||
trackOnboardingCompleted('local');
|
||||
setShowLocalModelSetup(false);
|
||||
setShowFirstTimeSetup(false);
|
||||
setHasProvider(true);
|
||||
navigate('/', { replace: true });
|
||||
};
|
||||
|
||||
const handleLocalModelCancel = () => {
|
||||
trackOnboardingAbandoned('local_model_setup');
|
||||
setShowLocalModelSetup(false);
|
||||
};
|
||||
|
||||
const handleRetrySetup = (setupType: 'openrouter' | 'tetrate' | 'chatgpt_codex') => {
|
||||
if (setupType === 'openrouter') {
|
||||
setOpenRouterSetupState(null);
|
||||
handleOpenRouterSetup();
|
||||
} else if (setupType === 'tetrate') {
|
||||
setTetrateSetupState(null);
|
||||
handleTetrateSetup();
|
||||
} else {
|
||||
setChatgptCodexSetupState(null);
|
||||
handleChatGptCodexSetup();
|
||||
}
|
||||
};
|
||||
|
||||
const closeSetupModal = (setupType: 'openrouter' | 'tetrate' | 'chatgpt_codex') => {
|
||||
if (setupType === 'openrouter') {
|
||||
setOpenRouterSetupState(null);
|
||||
} else if (setupType === 'tetrate') {
|
||||
setTetrateSetupState(null);
|
||||
} else {
|
||||
setChatgptCodexSetupState(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const checkProvider = async () => {
|
||||
try {
|
||||
const provider = ((await read('GOOSE_PROVIDER', false)) as string) || '';
|
||||
const hasConfiguredProvider = provider.trim() !== '';
|
||||
|
||||
// If user is actively testing keys, don't redirect
|
||||
if (userInActiveSetup) {
|
||||
setHasProvider(false);
|
||||
setShowFirstTimeSetup(true);
|
||||
} else if (hasConfiguredProvider || didSelectProvider) {
|
||||
setHasProvider(true);
|
||||
setShowFirstTimeSetup(false);
|
||||
} else {
|
||||
setHasProvider(false);
|
||||
setShowFirstTimeSetup(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking provider:', error);
|
||||
toastService.error({
|
||||
title: 'Configuration Error',
|
||||
msg: 'Failed to check provider configuration.',
|
||||
traceback: error instanceof Error ? error.stack || '' : '',
|
||||
});
|
||||
setHasProvider(false);
|
||||
setShowFirstTimeSetup(true);
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkProvider();
|
||||
}, [read, didSelectProvider, userInActiveSetup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isChecking && !hasProvider && showFirstTimeSetup && !onboardingTracked.current) {
|
||||
trackOnboardingStarted();
|
||||
onboardingTracked.current = true;
|
||||
}
|
||||
}, [isChecking, hasProvider, showFirstTimeSetup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isChecking && !hasProvider && showFirstTimeSetup) {
|
||||
// Check scroll position after content renders
|
||||
const timer = setTimeout(checkScrollPosition, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
return undefined;
|
||||
}, [isChecking, hasProvider, showFirstTimeSetup, checkScrollPosition]);
|
||||
|
||||
if (isChecking) {
|
||||
return (
|
||||
<div className="h-screen w-full bg-background-primary flex items-center justify-center">
|
||||
<WelcomeGooseLogo />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (showLocalModelSetup) {
|
||||
return (
|
||||
<div className="h-screen w-full bg-background-default overflow-hidden">
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="min-h-full flex flex-col items-center justify-center p-4 py-8">
|
||||
<div className="max-w-2xl w-full mx-auto p-8">
|
||||
<LocalModelSetup
|
||||
onSuccess={handleLocalModelComplete}
|
||||
onCancel={handleLocalModelCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasProvider && showFirstTimeSetup) {
|
||||
return (
|
||||
<div className="h-screen w-full bg-background-primary overflow-hidden relative">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
onScroll={checkScrollPosition}
|
||||
className="h-full overflow-y-auto"
|
||||
>
|
||||
<div className="min-h-full flex flex-col items-center justify-center p-4 py-8">
|
||||
<div className="max-w-2xl w-full mx-auto p-8">
|
||||
{/* Header section */}
|
||||
<div className="text-left mb-8 sm:mb-12">
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<div className="origin-bottom-left goose-icon-animation">
|
||||
<Goose className="size-6 sm:size-8" />
|
||||
</div>
|
||||
<h1 className="text-2xl sm:text-4xl font-light text-left">Welcome to Goose</h1>
|
||||
</div>
|
||||
<p className="text-text-secondary text-base sm:text-lg mt-4 sm:mt-6">
|
||||
Since it’s your first time here, let’s get you set up with an AI provider so goose
|
||||
can work its magic.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ApiKeyTester
|
||||
onSuccess={handleApiKeySuccess}
|
||||
onStartTesting={() => {
|
||||
setUserInActiveSetup(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Run Locally Card */}
|
||||
<div className="relative w-full mb-4">
|
||||
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-green-600 text-white rounded-full">
|
||||
Free & Private
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
trackOnboardingProviderSelected({ method: 'local' });
|
||||
setShowLocalModelSetup(true);
|
||||
}}
|
||||
className="w-full p-4 sm:p-6 bg-transparent border rounded-xl transition-all duration-200 cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-text-default text-sm sm:text-base">
|
||||
Run Locally
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-text-muted group-hover:text-text-default transition-colors">
|
||||
<svg
|
||||
className="w-4 h-4 sm:w-5 sm:h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-text-muted text-sm sm:text-base">
|
||||
Download a model and run entirely on your machine. No API keys, no accounts.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ChatGPT Subscription Card - Full Width */}
|
||||
<div className="relative w-full mb-4">
|
||||
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-blue-600 text-white rounded-full">
|
||||
Recommended if you have ChatGPT Plus/Pro
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={handleChatGptCodexSetup}
|
||||
className="w-full p-4 sm:p-6 bg-transparent border rounded-xl transition-all duration-200 cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ChatGPT className="w-5 h-5 text-text-primary" />
|
||||
<span className="font-medium text-text-primary text-sm sm:text-base">
|
||||
ChatGPT Subscription
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-text-secondary group-hover:text-text-primary transition-colors">
|
||||
<svg
|
||||
className="w-4 h-4 sm:w-5 sm:h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-text-secondary text-sm sm:text-base">
|
||||
Use your ChatGPT Plus/Pro subscription for GPT-5 Codex models.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tetrate Card - Full Width */}
|
||||
<div className="relative w-full mb-4">
|
||||
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-blue-600 text-white rounded-full">
|
||||
Recommended for new users
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={handleTetrateSetup}
|
||||
className="w-full p-4 sm:p-6 bg-transparent border rounded-xl transition-all duration-200 cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tetrate className="w-5 h-5 text-text-primary" />
|
||||
<span className="text-sm sm:text-base">
|
||||
<span className="font-medium text-text-primary">Agent Router</span>
|
||||
<span className="text-text-secondary text-xs"> by Tetrate</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-text-secondary group-hover:text-text-primary transition-colors">
|
||||
<svg
|
||||
className="w-4 h-4 sm:w-5 sm:h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-text-secondary text-sm sm:text-base">
|
||||
Access multiple AI models with automatic setup. Sign up to receive $10 credit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OpenRouter Card - Full Width */}
|
||||
<div
|
||||
onClick={handleOpenRouterSetup}
|
||||
className="relative w-full p-4 sm:p-6 bg-transparent border rounded-xl transition-all duration-200 cursor-pointer group overflow-hidden mb-6"
|
||||
>
|
||||
{/* Subtle shimmer effect */}
|
||||
<div className="absolute inset-0 -translate-x-full animate-shimmer bg-gradient-to-r from-transparent via-white/8 to-transparent"></div>
|
||||
|
||||
<div className="relative flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<OpenRouter className="w-5 h-5 text-text-primary" />
|
||||
<span className="font-medium text-text-primary text-sm sm:text-base">
|
||||
OpenRouter
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-text-secondary group-hover:text-text-primary transition-colors">
|
||||
<svg
|
||||
className="w-4 h-4 sm:w-5 sm:h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-text-secondary text-sm sm:text-base">
|
||||
Access 200+ models with one API. Pay-per-use pricing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Other providers section */}
|
||||
<div className="w-full p-4 sm:p-6 bg-transparent border rounded-xl">
|
||||
<h3 className="font-medium text-text-primary text-sm sm:text-base mb-3">
|
||||
Other Providers
|
||||
</h3>
|
||||
<p className="text-text-secondary text-sm sm:text-base mb-4">
|
||||
Set up additional providers manually through settings.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/welcome', { replace: true })}
|
||||
className="text-blue-600 hover:text-blue-500 text-sm font-medium transition-colors"
|
||||
>
|
||||
Go to Provider Settings →
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<TelemetrySettings isWelcome />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator - fixed at bottom, hides when scrolled to bottom */}
|
||||
{showScrollIndicator && (
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 pointer-events-none transition-opacity duration-300 opacity-60 animate-bounce">
|
||||
<div className="flex flex-col items-center gap-1 text-text-secondary">
|
||||
<span className="text-xs">More options below</span>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Setup Modals */}
|
||||
{openRouterSetupState?.show && (
|
||||
<SetupModal
|
||||
title={openRouterSetupState.title}
|
||||
message={openRouterSetupState.message}
|
||||
showRetry={openRouterSetupState.showRetry}
|
||||
onRetry={() => handleRetrySetup('openrouter')}
|
||||
onClose={() => closeSetupModal('openrouter')}
|
||||
autoClose={openRouterSetupState.autoClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{tetrateSetupState?.show && (
|
||||
<SetupModal
|
||||
title={tetrateSetupState.title}
|
||||
message={tetrateSetupState.message}
|
||||
showRetry={tetrateSetupState.showRetry}
|
||||
onRetry={() => handleRetrySetup('tetrate')}
|
||||
onClose={() => closeSetupModal('tetrate')}
|
||||
autoClose={tetrateSetupState.autoClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{chatgptCodexSetupState?.show && (
|
||||
<SetupModal
|
||||
title={chatgptCodexSetupState.title}
|
||||
message={chatgptCodexSetupState.message}
|
||||
showRetry={chatgptCodexSetupState.showRetry}
|
||||
onRetry={() => handleRetrySetup('chatgpt_codex')}
|
||||
onClose={() => closeSetupModal('chatgpt_codex')}
|
||||
autoClose={chatgptCodexSetupState.autoClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showSwitchModelModal && (
|
||||
<SwitchModelModal
|
||||
sessionId={null}
|
||||
onClose={handleSwitchModelClose}
|
||||
setView={setView}
|
||||
onModelSelected={handleModelSelected}
|
||||
initialProvider={switchModelProvider}
|
||||
titleOverride="Choose Model"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface SetupModalProps {
|
||||
title: string;
|
||||
message: string;
|
||||
showProgress?: boolean;
|
||||
showRetry?: boolean;
|
||||
onRetry?: () => void;
|
||||
autoClose?: number;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function SetupModal({
|
||||
title,
|
||||
message,
|
||||
showProgress,
|
||||
showRetry,
|
||||
onRetry,
|
||||
autoClose,
|
||||
onClose,
|
||||
}: SetupModalProps) {
|
||||
useEffect(() => {
|
||||
if (autoClose && onClose) {
|
||||
const timer = window.setTimeout(() => {
|
||||
onClose();
|
||||
}, autoClose);
|
||||
return () => window.clearTimeout(timer);
|
||||
}
|
||||
return undefined;
|
||||
}, [autoClose, onClose]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full">
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-gray-100">{title}</h2>
|
||||
<p className="mb-6 text-gray-700 dark:text-gray-300">{message}</p>
|
||||
|
||||
{showProgress && (
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onClose && (
|
||||
<div className="mb-4">
|
||||
<Button onClick={onClose} className="w-full">
|
||||
Close
|
||||
</Button>
|
||||
<br />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showRetry && onRetry && (
|
||||
<Button onClick={onRetry} className="w-full">
|
||||
Retry Setup
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Goose, Rain } from './icons/Goose';
|
||||
|
||||
export default function WelcomeGooseLogo({ className = '' }) {
|
||||
return (
|
||||
<div className={`${className} relative overflow-hidden`}>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Rain className="w-full h-full scale-[2.5] opacity-0 group-hover/logo:opacity-100 transition-all duration-300 z-1" />
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Goose className="w-full h-full z-2" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,6 @@ export default function ResetProviderSection(_props: ResetProviderSectionProps)
|
||||
await remove('GOOSE_PROVIDER', false);
|
||||
await remove('GOOSE_MODEL', false);
|
||||
|
||||
// Refresh the page to trigger the ProviderGuard check
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('Failed to reset provider and model:', error);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const USE_NEW_ONBOARDING = true;
|
||||
@@ -303,18 +303,6 @@ export function trackOnboardingCompleted(provider: string, model?: string): void
|
||||
onboardingStartTime = null;
|
||||
}
|
||||
|
||||
export function trackOnboardingAbandoned(step: string): void {
|
||||
const durationSeconds = onboardingStartTime
|
||||
? Math.round((Date.now() - onboardingStartTime) / 1000)
|
||||
: undefined;
|
||||
|
||||
trackEvent({
|
||||
name: 'onboarding_abandoned',
|
||||
properties: { step, duration_seconds: durationSeconds },
|
||||
});
|
||||
onboardingStartTime = null;
|
||||
}
|
||||
|
||||
export function trackOnboardingSetupFailed(
|
||||
provider: 'openrouter' | 'tetrate' | 'chatgpt_codex' | 'local',
|
||||
errorMessage?: string
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { configureProviderOauth } from '../api';
|
||||
|
||||
export async function startChatGptCodexSetup(): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await configureProviderOauth({
|
||||
path: { name: 'chatgpt_codex' },
|
||||
throwOnError: true,
|
||||
});
|
||||
return { success: true, message: 'ChatGPT Codex setup completed' };
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to start ChatGPT Codex setup: ${e}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { startOpenrouterSetup } from '../api';
|
||||
|
||||
export interface OpenRouterSetupStatus {
|
||||
isRunning: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export async function startOpenRouterSetup(): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
return (await startOpenrouterSetup({ throwOnError: true })).data;
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to start OpenRouter setup ['${e}]`,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user