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::status::diagnostics,
|
||||||
super::routes::mcp_ui_proxy::mcp_ui_proxy,
|
super::routes::mcp_ui_proxy::mcp_ui_proxy,
|
||||||
super::routes::config_management::backup_config,
|
super::routes::config_management::backup_config,
|
||||||
super::routes::config_management::detect_provider,
|
|
||||||
super::routes::config_management::recover_config,
|
super::routes::config_management::recover_config,
|
||||||
super::routes::config_management::validate_config,
|
super::routes::config_management::validate_config,
|
||||||
super::routes::config_management::init_config,
|
super::routes::config_management::init_config,
|
||||||
@@ -484,8 +483,6 @@ derive_utoipa!(Icon as IconSchema);
|
|||||||
components(schemas(
|
components(schemas(
|
||||||
super::routes::config_management::UpsertConfigQuery,
|
super::routes::config_management::UpsertConfigQuery,
|
||||||
super::routes::config_management::ConfigKeyQuery,
|
super::routes::config_management::ConfigKeyQuery,
|
||||||
super::routes::config_management::DetectProviderRequest,
|
|
||||||
super::routes::config_management::DetectProviderResponse,
|
|
||||||
super::routes::config_management::ConfigResponse,
|
super::routes::config_management::ConfigResponse,
|
||||||
super::routes::config_management::ProvidersResponse,
|
super::routes::config_management::ProvidersResponse,
|
||||||
super::routes::config_management::ProviderDetails,
|
super::routes::config_management::ProviderDetails,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use goose::config::paths::Paths;
|
|||||||
use goose::config::ExtensionEntry;
|
use goose::config::ExtensionEntry;
|
||||||
use goose::config::{Config, ConfigError};
|
use goose::config::{Config, ConfigError};
|
||||||
use goose::model::ModelConfig;
|
use goose::model::ModelConfig;
|
||||||
use goose::providers::auto_detect::detect_provider_from_api_key;
|
|
||||||
use goose::providers::base::{ProviderMetadata, ProviderType};
|
use goose::providers::base::{ProviderMetadata, ProviderType};
|
||||||
use goose::providers::canonical::maybe_get_canonical_model;
|
use goose::providers::canonical::maybe_get_canonical_model;
|
||||||
use goose::providers::catalog::{
|
use goose::providers::catalog::{
|
||||||
@@ -149,16 +148,6 @@ pub struct SlashCommandsResponse {
|
|||||||
pub commands: Vec<SlashCommand>,
|
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(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/config/upsert",
|
path = "/config/upsert",
|
||||||
@@ -534,31 +523,6 @@ pub async fn upsert_permissions(
|
|||||||
Ok(Json("Permissions updated successfully".to_string()))
|
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(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/config/backup",
|
path = "/config/backup",
|
||||||
@@ -930,7 +894,6 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
|||||||
"/config/providers/{name}/cleanup",
|
"/config/providers/{name}/cleanup",
|
||||||
post(cleanup_provider_cache),
|
post(cleanup_provider_cache),
|
||||||
)
|
)
|
||||||
.route("/config/detect-provider", post(detect_provider))
|
|
||||||
.route("/config/slash_commands", get(get_slash_commands))
|
.route("/config/slash_commands", get(get_slash_commands))
|
||||||
.route(
|
.route(
|
||||||
"/config/canonical-model-info",
|
"/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 anthropic;
|
||||||
pub mod api_client;
|
pub mod api_client;
|
||||||
pub mod auto_detect;
|
|
||||||
pub mod avian;
|
pub mod avian;
|
||||||
pub mod azure;
|
pub mod azure;
|
||||||
pub mod azureauth;
|
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": {
|
"/config/extensions": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"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": {
|
"DictationProvider": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|||||||
@@ -61,10 +61,6 @@ vi.mock('./sessions', () => ({
|
|||||||
generateSessionId: vi.fn(),
|
generateSessionId: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./utils/openRouterSetup', () => ({
|
|
||||||
startOpenRouterSetup: vi.fn().mockResolvedValue({ success: false, message: 'Test' }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock the ConfigContext module
|
// Mock the ConfigContext module
|
||||||
vi.mock('./components/ConfigContext', () => ({
|
vi.mock('./components/ConfigContext', () => ({
|
||||||
useConfig: () => ({
|
useConfig: () => ({
|
||||||
@@ -83,19 +79,6 @@ vi.mock('./components/ErrorBoundary', () => ({
|
|||||||
ErrorUI: ({ error }: { error: Error }) => <div>Error: {error.message}</div>,
|
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', () => ({
|
vi.mock('./components/ModelAndProviderContext', () => ({
|
||||||
ModelAndProviderProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
ModelAndProviderProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||||
useModelAndProvider: () => ({
|
useModelAndProvider: () => ({
|
||||||
|
|||||||
+7
-26
@@ -15,9 +15,7 @@ import { ExtensionInstallModal } from './components/ExtensionInstallModal';
|
|||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
import AnnouncementModal from './components/AnnouncementModal';
|
import AnnouncementModal from './components/AnnouncementModal';
|
||||||
import TelemetryOptOutModal from './components/TelemetryOptOutModal';
|
import TelemetryOptOutModal from './components/TelemetryOptOutModal';
|
||||||
import ProviderGuard from './components/ProviderGuard';
|
|
||||||
import OnboardingGuard from './components/onboarding/OnboardingGuard';
|
import OnboardingGuard from './components/onboarding/OnboardingGuard';
|
||||||
import { USE_NEW_ONBOARDING } from './featureFlags';
|
|
||||||
import { createSession } from './sessions';
|
import { createSession } from './sessions';
|
||||||
|
|
||||||
import { ChatType } from './types/chat';
|
import { ChatType } from './types/chat';
|
||||||
@@ -249,11 +247,7 @@ const ConfigureProvidersRoute = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface WelcomeRouteProps {
|
const WelcomeRoute = () => {
|
||||||
onSelectProvider: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WelcomeRoute = ({ onSelectProvider }: WelcomeRouteProps) => {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -265,7 +259,6 @@ const WelcomeRoute = ({ onSelectProvider }: WelcomeRouteProps) => {
|
|||||||
isOnboarding={true}
|
isOnboarding={true}
|
||||||
onProviderLaunched={(model?: string) => {
|
onProviderLaunched={(model?: string) => {
|
||||||
trackOnboardingCompleted('other', model);
|
trackOnboardingCompleted('other', model);
|
||||||
onSelectProvider();
|
|
||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -351,7 +344,6 @@ export function AppInner() {
|
|||||||
const [fatalError, setFatalError] = useState<string | null>(null);
|
const [fatalError, setFatalError] = useState<string | null>(null);
|
||||||
const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false);
|
const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false);
|
||||||
const [sharedSessionError, setSharedSessionError] = useState<string | null>(null);
|
const [sharedSessionError, setSharedSessionError] = useState<string | null>(null);
|
||||||
const [didSelectProvider, setDidSelectProvider] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const setView = useNavigation();
|
const setView = useNavigation();
|
||||||
@@ -650,28 +642,17 @@ export function AppInner() {
|
|||||||
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="launcher" element={<LauncherView />} />
|
<Route path="launcher" element={<LauncherView />} />
|
||||||
<Route
|
<Route path="welcome" element={<WelcomeRoute />} />
|
||||||
path="welcome"
|
|
||||||
element={<WelcomeRoute onSelectProvider={() => setDidSelectProvider(true)} />}
|
|
||||||
/>
|
|
||||||
<Route path="configure-providers" element={<ConfigureProvidersRoute />} />
|
<Route path="configure-providers" element={<ConfigureProvidersRoute />} />
|
||||||
<Route path="standalone-app" element={<StandaloneAppView />} />
|
<Route path="standalone-app" element={<StandaloneAppView />} />
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
USE_NEW_ONBOARDING ? (
|
<OnboardingGuard>
|
||||||
<OnboardingGuard>
|
<ChatProvider chat={chat} setChat={setChat} contextKey="hub">
|
||||||
<ChatProvider chat={chat} setChat={setChat} contextKey="hub">
|
<AppLayout activeSessions={activeSessions} />
|
||||||
<AppLayout activeSessions={activeSessions} />
|
</ChatProvider>
|
||||||
</ChatProvider>
|
</OnboardingGuard>
|
||||||
</OnboardingGuard>
|
|
||||||
) : (
|
|
||||||
<ProviderGuard didSelectProvider={didSelectProvider}>
|
|
||||||
<ChatProvider chat={chat} setChat={setChat} contextKey="hub">
|
|
||||||
<AppLayout activeSessions={activeSessions} />
|
|
||||||
</ChatProvider>
|
|
||||||
</ProviderGuard>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route index element={<HubRouteWrapper />} />
|
<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;
|
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 DictationProvider = 'openai' | 'elevenlabs' | 'groq' | 'local';
|
||||||
|
|
||||||
export type DictationProviderStatus = {
|
export type DictationProviderStatus = {
|
||||||
@@ -2356,29 +2347,6 @@ export type UpdateCustomProviderResponses = {
|
|||||||
|
|
||||||
export type UpdateCustomProviderResponse = UpdateCustomProviderResponses[keyof 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 = {
|
export type GetExtensionsData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path?: 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_PROVIDER', false);
|
||||||
await remove('GOOSE_MODEL', false);
|
await remove('GOOSE_MODEL', false);
|
||||||
|
|
||||||
// Refresh the page to trigger the ProviderGuard check
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to reset provider and model:', 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;
|
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(
|
export function trackOnboardingSetupFailed(
|
||||||
provider: 'openrouter' | 'tetrate' | 'chatgpt_codex' | 'local',
|
provider: 'openrouter' | 'tetrate' | 'chatgpt_codex' | 'local',
|
||||||
errorMessage?: string
|
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