Skip to content

Commit 3fd958f

Browse files
vijaythecoderclaude
andcommitted
Add clean onboarding flow with API key setup and GitHub star request
- Create beautiful onboarding page with minimal design - Add CheckOnboarding middleware to redirect users without API key - Implement two-step flow: API key entry + GitHub support request - Add API endpoint for opening external URLs in default browser - Remove unnecessary skip option - API key is required - Update navigation to go to realtime-agent after setup 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 8fd4440 commit 3fd958f

File tree

6 files changed

+355
-0
lines changed

6 files changed

+355
-0
lines changed

app/Http/Controllers/Settings/ApiKeyController.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,32 @@ public function destroy(Request $request): RedirectResponse
6666

6767
return redirect()->route('api-keys.edit')->with('success', 'API key deleted successfully.');
6868
}
69+
70+
/**
71+
* Store the OpenAI API key (used by onboarding)
72+
*/
73+
public function store(Request $request)
74+
{
75+
$request->validate([
76+
'api_key' => ['required', 'string', 'min:20'],
77+
]);
78+
79+
$apiKey = $request->input('api_key');
80+
81+
// Validate the API key
82+
if (!$this->apiKeyService->validateApiKey($apiKey)) {
83+
return response()->json([
84+
'success' => false,
85+
'message' => 'The provided API key is invalid. Please check and try again.',
86+
], 422);
87+
}
88+
89+
// Store the API key
90+
$this->apiKeyService->setApiKey($apiKey);
91+
92+
return response()->json([
93+
'success' => true,
94+
'message' => 'API key saved successfully.',
95+
]);
96+
}
6997
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use App\Services\ApiKeyService;
6+
use Closure;
7+
use Illuminate\Http\Request;
8+
use Symfony\Component\HttpFoundation\Response;
9+
10+
class CheckOnboarding
11+
{
12+
protected ApiKeyService $apiKeyService;
13+
14+
public function __construct(ApiKeyService $apiKeyService)
15+
{
16+
$this->apiKeyService = $apiKeyService;
17+
}
18+
19+
/**
20+
* Handle an incoming request.
21+
*
22+
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
23+
*/
24+
public function handle(Request $request, Closure $next): Response
25+
{
26+
// Routes that should be accessible without API key
27+
$excludedRoutes = [
28+
'onboarding',
29+
'api-keys.edit',
30+
'api-keys.update',
31+
'api-keys.destroy',
32+
'api.openai.status',
33+
'api.openai.api-key.store',
34+
'appearance',
35+
];
36+
37+
// Skip check for excluded routes
38+
if ($request->route() && in_array($request->route()->getName(), $excludedRoutes)) {
39+
return $next($request);
40+
}
41+
42+
// Skip if API request (they handle their own errors)
43+
if ($request->is('api/*')) {
44+
return $next($request);
45+
}
46+
47+
// Check if API key exists
48+
if (!$this->apiKeyService->hasApiKey()) {
49+
// If not on onboarding page and no API key, redirect to onboarding
50+
if ($request->route() && $request->route()->getName() !== 'onboarding') {
51+
return redirect()->route('onboarding');
52+
}
53+
}
54+
55+
return $next($request);
56+
}
57+
}

bootstrap/app.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use App\Http\Middleware\CheckOnboarding;
34
use App\Http\Middleware\HandleAppearance;
45
use App\Http\Middleware\HandleInertiaRequests;
56
use Illuminate\Foundation\Application;
@@ -17,6 +18,7 @@
1718
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
1819

1920
$middleware->web(append: [
21+
CheckOnboarding::class,
2022
HandleAppearance::class,
2123
HandleInertiaRequests::class,
2224
AddLinkHeadersForPreloadedAssets::class,

resources/js/pages/Onboarding.vue

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<template>
2+
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
3+
<div class="flex min-h-screen items-center justify-center p-6">
4+
<div class="w-full max-w-md">
5+
<!-- Logo and App Name -->
6+
<div class="mb-8 text-center">
7+
<div class="mb-4 flex items-center justify-center">
8+
<div class="rounded-xl bg-gray-900 p-3 dark:bg-gray-100">
9+
<svg class="h-10 w-10 text-white dark:text-gray-900" fill="none" stroke="currentColor" viewBox="0 0 24 24">
10+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
11+
</svg>
12+
</div>
13+
</div>
14+
<h1 class="text-3xl font-semibold text-gray-900 dark:text-white">Welcome to Clueless</h1>
15+
<p class="mt-2 text-gray-600 dark:text-gray-400">AI-powered meeting assistant</p>
16+
</div>
17+
18+
<!-- Progress Indicator -->
19+
<div v-if="showSteps" class="mb-6 flex items-center justify-center space-x-2">
20+
<div v-for="i in 2" :key="i"
21+
:class="['h-1.5 w-20 rounded-full transition-all duration-300',
22+
i <= currentStep ? 'bg-gray-900 dark:bg-gray-100' : 'bg-gray-200 dark:bg-gray-700']">
23+
</div>
24+
</div>
25+
26+
<!-- Main Card -->
27+
<div class="rounded-xl bg-white shadow-sm ring-1 ring-gray-200 dark:bg-gray-800 dark:ring-gray-700">
28+
<div class="p-6">
29+
<!-- Step 1: API Key -->
30+
<div v-if="currentStep === 1">
31+
<h2 class="mb-6 text-xl font-medium text-gray-900 dark:text-white">Setup OpenAI API</h2>
32+
33+
<div class="space-y-4">
34+
<div>
35+
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
36+
API Key
37+
</label>
38+
<div class="relative">
39+
<input
40+
v-model="apiKey"
41+
:type="showApiKey ? 'text' : 'password'"
42+
placeholder="sk-..."
43+
class="w-full rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-gray-900 placeholder-gray-400 focus:border-gray-400 focus:outline-none focus:ring-1 focus:ring-gray-400 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-500"
44+
@input="validateApiKey"
45+
/>
46+
<button
47+
@click="showApiKey = !showApiKey"
48+
type="button"
49+
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
50+
>
51+
<svg v-if="!showApiKey" class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
52+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
53+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
54+
</svg>
55+
<svg v-else class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
56+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
57+
</svg>
58+
</button>
59+
</div>
60+
<Transition
61+
enter-active-class="transition duration-200 ease-out"
62+
enter-from-class="opacity-0"
63+
enter-to-class="opacity-100"
64+
leave-active-class="transition duration-150 ease-in"
65+
leave-from-class="opacity-100"
66+
leave-to-class="opacity-0"
67+
>
68+
<p v-if="apiKeyError" class="mt-1.5 text-sm text-red-600 dark:text-red-400">
69+
{{ apiKeyError }}
70+
</p>
71+
<p v-else-if="apiKeyValid" class="mt-1.5 text-sm text-green-600 dark:text-green-400">
72+
✓ Valid API key format
73+
</p>
74+
</Transition>
75+
</div>
76+
77+
<div class="rounded-lg bg-gray-50 p-3 dark:bg-gray-900/50">
78+
<p class="text-sm text-gray-600 dark:text-gray-400">
79+
Need an API key?
80+
<a href="https://platform.openai.com/api-keys" target="_blank" class="font-medium text-gray-900 underline hover:text-gray-700 dark:text-gray-100 dark:hover:text-gray-300">
81+
Get one from OpenAI
82+
</a>
83+
</p>
84+
</div>
85+
</div>
86+
87+
<div class="mt-6 flex justify-end">
88+
<button
89+
@click="saveApiKey"
90+
:disabled="!apiKeyValid || isValidating"
91+
:class="['rounded-lg px-6 py-2.5 text-sm font-medium transition-all',
92+
apiKeyValid && !isValidating
93+
? 'bg-gray-900 text-white hover:bg-gray-800 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200'
94+
: 'bg-gray-100 text-gray-400 cursor-not-allowed dark:bg-gray-800 dark:text-gray-600']"
95+
>
96+
<span v-if="isValidating" class="flex items-center">
97+
<svg class="mr-2 h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
98+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
99+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
100+
</svg>
101+
Validating...
102+
</span>
103+
<span v-else>Continue</span>
104+
</button>
105+
</div>
106+
</div>
107+
108+
<!-- Step 2: GitHub Star -->
109+
<div v-else-if="currentStep === 2">
110+
<div class="text-center">
111+
<div class="mb-4 inline-flex rounded-full bg-gray-100 p-3 dark:bg-gray-700">
112+
<svg class="h-8 w-8 text-gray-700 dark:text-gray-300" fill="currentColor" viewBox="0 0 24 24">
113+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
114+
</svg>
115+
</div>
116+
<h2 class="mb-2 text-xl font-medium text-gray-900 dark:text-white">Support Clueless</h2>
117+
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
118+
If you find Clueless helpful, please consider starring our repository on GitHub. It helps others discover the project!
119+
</p>
120+
121+
<button
122+
@click="openGitHub"
123+
class="mb-4 inline-flex items-center rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-medium text-white hover:bg-gray-800 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200"
124+
>
125+
<svg class="mr-2 h-4 w-4" fill="currentColor" viewBox="0 0 16 16">
126+
<path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
127+
</svg>
128+
Star on GitHub
129+
</button>
130+
131+
<Transition
132+
enter-active-class="transition duration-200 ease-out"
133+
enter-from-class="opacity-0 scale-95"
134+
enter-to-class="opacity-100 scale-100"
135+
>
136+
<p v-if="hasStarred" class="mb-4 text-sm text-green-600 dark:text-green-400">
137+
✓ Thank you for your support!
138+
</p>
139+
</Transition>
140+
</div>
141+
142+
<div class="mt-6 flex items-center justify-between">
143+
<button
144+
@click="currentStep = 1"
145+
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
146+
>
147+
Back
148+
</button>
149+
<button
150+
@click="completeOnboarding"
151+
class="rounded-lg bg-gray-900 px-6 py-2.5 text-sm font-medium text-white hover:bg-gray-800 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200"
152+
>
153+
Get Started
154+
</button>
155+
</div>
156+
</div>
157+
</div>
158+
</div>
159+
</div>
160+
</div>
161+
</div>
162+
</template>
163+
164+
<script setup lang="ts">
165+
import { ref, computed } from 'vue'
166+
import { router } from '@inertiajs/vue3'
167+
import axios from 'axios'
168+
169+
const currentStep = ref(1)
170+
const apiKey = ref('')
171+
const showApiKey = ref(false)
172+
const apiKeyValid = ref(false)
173+
const apiKeyError = ref('')
174+
const isValidating = ref(false)
175+
const hasStarred = ref(false)
176+
177+
const showSteps = computed(() => currentStep.value > 0)
178+
179+
const validateApiKey = () => {
180+
const key = apiKey.value.trim()
181+
182+
if (!key) {
183+
apiKeyValid.value = false
184+
apiKeyError.value = ''
185+
return
186+
}
187+
188+
if (!key.startsWith('sk-')) {
189+
apiKeyValid.value = false
190+
apiKeyError.value = 'API key should start with "sk-"'
191+
return
192+
}
193+
194+
if (key.length < 20) {
195+
apiKeyValid.value = false
196+
apiKeyError.value = 'API key seems too short'
197+
return
198+
}
199+
200+
apiKeyValid.value = true
201+
apiKeyError.value = ''
202+
}
203+
204+
const saveApiKey = async () => {
205+
if (!apiKeyValid.value || isValidating.value) return
206+
207+
isValidating.value = true
208+
209+
try {
210+
const response = await axios.post('/api/openai/api-key', {
211+
api_key: apiKey.value
212+
})
213+
214+
if (response.data.success) {
215+
currentStep.value = 2
216+
} else {
217+
apiKeyError.value = 'Failed to save API key. Please try again.'
218+
}
219+
} catch (error) {
220+
apiKeyError.value = 'Invalid API key or connection error. Please check and try again.'
221+
} finally {
222+
isValidating.value = false
223+
}
224+
}
225+
226+
const openGitHub = async () => {
227+
try {
228+
// Use NativePHP API endpoint to open in default browser
229+
await axios.post('/api/open-external', {
230+
url: 'https://github.com/vijaythecoder/clueless'
231+
})
232+
hasStarred.value = true
233+
} catch (error) {
234+
console.error('Failed to open GitHub:', error)
235+
// Fallback for web browser
236+
window.open('https://github.com/vijaythecoder/clueless', '_blank')
237+
hasStarred.value = true
238+
}
239+
}
240+
241+
const completeOnboarding = () => {
242+
// Navigate to realtime agent
243+
router.visit('/realtime-agent')
244+
}
245+
</script>

routes/settings.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
Route::put('settings/api-keys', [ApiKeyController::class, 'update'])->name('api-keys.update');
1010
Route::delete('settings/api-keys', [ApiKeyController::class, 'destroy'])->name('api-keys.destroy');
1111

12+
// API endpoint for saving API key (used by onboarding)
13+
Route::post('/api/openai/api-key', [ApiKeyController::class, 'store'])->name('api.openai.api-key.store');
14+
1215
Route::get('settings/appearance', function () {
1316
return Inertia::render('settings/Appearance');
1417
})->name('appearance');

routes/web.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
return Inertia::render('Welcome');
1515
})->name('home');
1616

17+
// Onboarding Route
18+
Route::get('/onboarding', function () {
19+
return Inertia::render('Onboarding');
20+
})->name('onboarding');
21+
1722
Route::get('dashboard', function () {
1823
return Inertia::render('Dashboard');
1924
})->name('dashboard');
@@ -40,6 +45,21 @@
4045
]);
4146
})->name('api.openai.status');
4247

48+
// Open external URL in default browser (for NativePHP)
49+
Route::post('/api/open-external', function (\Illuminate\Http\Request $request) {
50+
$url = $request->input('url');
51+
52+
// Validate URL
53+
if (!filter_var($url, FILTER_VALIDATE_URL)) {
54+
return response()->json(['error' => 'Invalid URL'], 400);
55+
}
56+
57+
// Use NativePHP Shell to open in default browser
58+
\Native\Laravel\Facades\Shell::openExternal($url);
59+
60+
return response()->json(['success' => true]);
61+
})->name('api.open-external');
62+
4363
// Template Routes
4464
Route::get('/templates', [\App\Http\Controllers\TemplateController::class, 'index'])
4565
->name('templates.index');

0 commit comments

Comments
 (0)