From 241833837b9f37e48177d3bcc882877af4f03656 Mon Sep 17 00:00:00 2001 From: Vijay Tupakula Date: Sun, 13 Jul 2025 10:46:07 -0500 Subject: [PATCH 1/6] feat: Add OpenAI API key onboarding and remove unused Assistant feature BREAKING CHANGES: - Removed Assistant feature (Whisper-based transcription) - Removed chat functionality - Application now focuses exclusively on Realtime Agent Added: - User-specific OpenAI API key management - Onboarding flow for new users to configure API keys - Settings page for managing API keys - Secure API key storage (encrypted in database) Removed: - Assistant page and all related components - AudioRecorder component - AssistantController and AIService - Whisper transcription endpoints - Chat functionality - Assistant-specific database tables (context_snapshots, conversations) Changes: - Dashboard now highlights Realtime Agent as the primary feature - Updated navigation to remove Assistant references - All OpenAI interactions now use user's personal API key - Improved security by removing API key from browser in production Technical details: - Added migration for openai_api_key and has_completed_onboarding fields - Created EnsureOnboardingCompleted middleware - Added OnboardingController for API key setup flow - Created migration to drop unused Assistant tables - Cleaned up routes and removed unused endpoints The application now provides a cleaner, more focused experience centered on the Realtime Agent feature with WebSocket-based voice conversations and real-time sales coaching. --- CLAUDE.md | 58 +- app/Console/Commands/NativeConfigCommand.php | 3 +- app/Console/Commands/NativePhpIniCommand.php | 3 +- app/Http/Controllers/AssistantController.php | 217 ------- .../Controllers/ConversationController.php | 22 +- app/Http/Controllers/OnboardingController.php | 81 +++ app/Http/Controllers/RealtimeController.php | 100 +-- .../Controllers/Settings/ApiKeyController.php | 70 +++ app/Http/Controllers/VariableController.php | 44 +- .../Middleware/EnsureOnboardingCompleted.php | 30 + app/Models/ContextSnapshot.php | 26 - app/Models/Conversation.php | 32 - app/Models/ConversationSession.php | 1 + app/Models/ConversationTranscript.php | 4 +- app/Models/Transcript.php | 10 +- app/Models/User.php | 5 + app/Models/Variable.php | 4 +- app/Providers/NativeAppServiceProvider.php | 8 +- app/Services/AIService.php | 178 ------ app/Services/RealtimeRelayService.php | 70 ++- app/Services/TemplateVariableResolver.php | 70 +-- app/Services/VariableService.php | 44 +- bootstrap/app.php | 7 +- ...045_create_conversation_sessions_table.php | 10 +- ..._create_conversation_transcripts_table.php | 2 +- ...053_create_conversation_insights_table.php | 2 +- ...25_07_08_040023_create_variables_table.php | 2 +- ...1307_add_openai_api_key_to_users_table.php | 29 + ...025_07_13_153706_drop_assistant_tables.php | 48 ++ database/seeders/MeetingTemplateSeeder.php | 42 +- resources/js/components/AudioRecorder.vue | 271 -------- resources/js/layouts/settings/Layout.vue | 4 + resources/js/pages/Assistant/Main.vue | 591 ------------------ resources/js/pages/Dashboard.vue | 97 ++- resources/js/pages/Onboarding/Welcome.vue | 119 ++++ resources/js/pages/settings/ApiKeys.vue | 155 +++++ routes/settings.php | 7 +- routes/web.php | 34 +- 38 files changed, 923 insertions(+), 1577 deletions(-) delete mode 100644 app/Http/Controllers/AssistantController.php create mode 100644 app/Http/Controllers/OnboardingController.php create mode 100644 app/Http/Controllers/Settings/ApiKeyController.php create mode 100644 app/Http/Middleware/EnsureOnboardingCompleted.php delete mode 100644 app/Models/ContextSnapshot.php delete mode 100644 app/Models/Conversation.php delete mode 100644 app/Services/AIService.php create mode 100644 database/migrations/2025_07_13_021307_add_openai_api_key_to_users_table.php create mode 100644 database/migrations/2025_07_13_153706_drop_assistant_tables.php delete mode 100644 resources/js/components/AudioRecorder.vue delete mode 100644 resources/js/pages/Assistant/Main.vue create mode 100644 resources/js/pages/Onboarding/Welcome.vue create mode 100644 resources/js/pages/settings/ApiKeys.vue diff --git a/CLAUDE.md b/CLAUDE.md index 49b1cd1..ca65765 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Project Overview + +Clueless is an AI-powered meeting assistant that provides real-time transcription, intelligent analysis, and action item extraction from conversations. It's built as a desktop application using Electron/NativePHP with support for multiple AI providers (OpenAI, Anthropic, Gemini). + ## Tech Stack - **Backend**: Laravel 12.0 (PHP 8.2+) @@ -10,7 +14,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **Build**: Vite 6 - **Desktop**: NativePHP/Electron - **Testing**: Pest PHP -- **Database**: SQLite (default) +- **Database**: SQLite (dual database setup) +- **Real-time**: OpenAI Realtime API, WebSockets +- **AI Integration**: OpenAI PHP, multiple provider support ## Development Commands @@ -69,6 +75,15 @@ php artisan test tests/Feature/DashboardTest.php # Run tests with coverage (if configured) php artisan test --coverage + +# Run tests in parallel +php artisan test --parallel + +# Run only unit tests +php artisan test --testsuite=Unit + +# Run only feature tests +php artisan test --testsuite=Feature ``` ## Project Architecture @@ -76,15 +91,17 @@ php artisan test --coverage ### Directory Structure - `/app/` - Laravel backend logic - - `/Http/Controllers/` - Request handlers (Auth, Settings) - - `/Models/` - Eloquent models + - `/Http/Controllers/` - Request handlers (Auth, Conversations, Realtime, Settings) + - `/Models/` - Eloquent models (User, Conversation, Transcript, etc.) + - `/Services/` - Business logic (AIService, RealtimeRelayService, TranscriptionService) - `/Providers/` - Service providers - `/resources/js/` - Vue frontend application - `/components/` - Reusable Vue components - - `/components/ui/` - UI component library (shadcn/ui-inspired) + - `/components/ui/` - UI component library (Reka UI based, shadcn/ui-inspired) - `/pages/` - Inertia.js page components - - `/layouts/` - Layout components - - `/composables/` - Vue composables (e.g., useAppearance) + - `/layouts/` - Layout components (App, Auth, Settings) + - `/composables/` - Vue composables (useAppearance, useRealtime, etc.) + - `/services/` - Frontend services (audioCaptureService, realtimeClient) - `/types/` - TypeScript type definitions - `/routes/` - Application routing (web.php) - `/database/` - Migrations, factories, seeders @@ -97,6 +114,8 @@ php artisan test --coverage 3. **TypeScript**: Strict mode enabled, with path alias `@/` for `/resources/js/` 4. **Authentication**: Built-in Laravel auth with custom Vue components 5. **Theme Support**: Dark/light mode via `useAppearance` composable +6. **Real-time Features**: WebSocket connections for live transcription using OpenAI Realtime API +7. **Service Architecture**: Core business logic separated into service classes (AIService, TranscriptionService, etc.) ### Important Files @@ -167,9 +186,34 @@ Example usage: - For Laravel docs: Use Context7 to get the latest Laravel 12 documentation - For any other library: First use `mcp__context7__resolve-library-id` to find the library +## Working with Real-time Features + +The application uses OpenAI's Realtime API for live transcription: +- Frontend audio capture: `/resources/js/services/audioCaptureService.ts` +- WebSocket client: `/resources/js/services/realtimeClient.ts` +- Backend relay service: `/app/Services/RealtimeRelayService.php` +- Controllers: `/app/Http/Controllers/RealtimeController.php` + +## AI Provider Integration + +Multiple AI providers are supported through environment configuration: +- OpenAI (GPT-4, Realtime API) +- Anthropic (Claude) +- Google (Gemini) +- Configuration: Update `.env` with appropriate API keys + +## License & Usage + +This project uses MIT License with Commons Clause: +- ✅ Free for personal and internal business use +- ❌ Commercial use (SaaS, resale) requires commercial agreement +- See `COMMONS_CLAUSE.md` for details + ## Notes - ESLint ignores `/resources/js/components/ui/*` (third-party UI components) - Prettier formats all files in `/resources/` directory - NativePHP allows running as desktop application -- Concurrently runs multiple processes in development (server, queue, logs, vite) \ No newline at end of file +- Concurrently runs multiple processes in development (server, queue, logs, vite) +- Built primarily for macOS desktop usage +- Uses Pusher for additional real-time features \ No newline at end of file diff --git a/app/Console/Commands/NativeConfigCommand.php b/app/Console/Commands/NativeConfigCommand.php index 35441a4..c8a90c8 100644 --- a/app/Console/Commands/NativeConfigCommand.php +++ b/app/Console/Commands/NativeConfigCommand.php @@ -27,6 +27,7 @@ public function handle() { // Return empty JSON to satisfy NativePHP expectations echo json_encode([]); + return 0; } -} \ No newline at end of file +} diff --git a/app/Console/Commands/NativePhpIniCommand.php b/app/Console/Commands/NativePhpIniCommand.php index aa14d8a..dd69985 100644 --- a/app/Console/Commands/NativePhpIniCommand.php +++ b/app/Console/Commands/NativePhpIniCommand.php @@ -32,6 +32,7 @@ public function handle() 'post_max_size' => '50M', ]; echo json_encode($settings); + return 0; } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/AssistantController.php b/app/Http/Controllers/AssistantController.php deleted file mode 100644 index 231afec..0000000 --- a/app/Http/Controllers/AssistantController.php +++ /dev/null @@ -1,217 +0,0 @@ -validate([ - 'message' => 'required|string', - ]); - - try { - // Get AI response - $response = $this->aiService->chat( - $request->input('message'), - [] - ); - - return response()->json([ - 'response' => $response['content'] ?? 'I apologize, but I was unable to generate a response.', - ]); - } catch (\Exception $e) { - Log::error('Chat error: ' . $e->getMessage()); - - return response()->json([ - 'error' => 'Failed to get AI response' - ], 500); - } - } - - public function transcribe(Request $request) - { - $request->validate([ - 'audio' => 'required|string', - ]); - - try { - $audioData = $request->input('audio'); - - // Use OpenAI Whisper for transcription - $result = $this->aiService->transcribe($audioData); - - return response()->json([ - 'text' => $result['text'] ?? '', - ]); - } catch (\Exception $e) { - Log::error('Transcription error: ' . $e->getMessage()); - - return response()->json([ - 'error' => 'Failed to transcribe audio' - ], 500); - } - } - - public function analyzeConversation(Request $request) - { - $request->validate([ - 'audio' => 'required|string', - 'context' => 'nullable|string', - 'mode' => 'required|in:sales-coaching', - ]); - - try { - $audioData = $request->input('audio'); - $context = $request->input('context', ''); - - // Ensure context is never null - if (is_null($context)) { - $context = ''; - } - - // First, transcribe the audio - $transcriptionResult = $this->aiService->transcribe($audioData); - $transcript = $transcriptionResult['text'] ?? ''; - - if (empty($transcript)) { - return response()->json([ - 'transcript' => '', - 'suggestions' => [], - 'metrics' => null - ]); - } - - // Analyze the conversation for sales coaching - $analysis = $this->aiService->analyzeSalesConversation($transcript, $context); - - return response()->json([ - 'transcript' => $transcript, - 'suggestions' => $analysis['suggestions'] ?? [], - 'metrics' => $analysis['metrics'] ?? null - ]); - } catch (\Exception $e) { - Log::error('Conversation analysis error: ' . $e->getMessage()); - - return response()->json([ - 'error' => 'Failed to analyze conversation' - ], 500); - } - } - - public function analyzeConversationStream(Request $request) - { - $request->validate([ - 'audio' => 'required|string', - 'context' => 'nullable|string', - 'mode' => 'required|in:sales-coaching', - 'sessionId' => 'nullable|string', - ]); - - return response()->stream(function () use ($request) { - // Set up SSE headers - header('Content-Type: text/event-stream'); - header('Cache-Control: no-cache'); - header('Connection: keep-alive'); - header('X-Accel-Buffering: no'); // Disable Nginx buffering - - // Send initial connection event - echo "data: " . json_encode(['type' => 'connected']) . "\n\n"; - ob_flush(); - flush(); - - try { - $audioData = $request->input('audio'); - $context = $request->input('context', ''); - $sessionId = $request->input('sessionId'); - - // Ensure context is never null - if (is_null($context)) { - $context = ''; - } - - // Send processing event - echo "data: " . json_encode([ - 'type' => 'processing', - 'sessionId' => $sessionId, - 'timestamp' => now()->toIso8601String() - ]) . "\n\n"; - ob_flush(); - flush(); - - // Transcribe the audio chunk - $transcriptionResult = $this->aiService->transcribe($audioData); - $transcript = $transcriptionResult['text'] ?? ''; - - if (!empty($transcript)) { - // Send transcription event - echo "data: " . json_encode([ - 'type' => 'transcription', - 'text' => $transcript, - 'sessionId' => $sessionId, - 'timestamp' => now()->toIso8601String() - ]) . "\n\n"; - ob_flush(); - flush(); - - // Analyze the conversation - $analysis = $this->aiService->analyzeSalesConversation($transcript, $context); - - // Send analysis event - echo "data: " . json_encode([ - 'type' => 'analysis', - 'suggestions' => $analysis['suggestions'] ?? [], - 'metrics' => $analysis['metrics'] ?? null, - 'sessionId' => $sessionId, - 'timestamp' => now()->toIso8601String() - ]) . "\n\n"; - ob_flush(); - flush(); - } else { - // Send empty transcription event - echo "data: " . json_encode([ - 'type' => 'transcription', - 'text' => '', - 'sessionId' => $sessionId, - 'timestamp' => now()->toIso8601String() - ]) . "\n\n"; - ob_flush(); - flush(); - } - - } catch (\Exception $e) { - // Send error event - Log::error('Stream processing error: ' . $e->getMessage()); - echo "data: " . json_encode([ - 'type' => 'error', - 'message' => 'Processing error occurred', - 'error' => $e->getMessage(), - 'timestamp' => now()->toIso8601String() - ]) . "\n\n"; - ob_flush(); - flush(); - } - - // Send completion event - echo "data: " . json_encode([ - 'type' => 'complete', - 'timestamp' => now()->toIso8601String() - ]) . "\n\n"; - ob_flush(); - flush(); - }, 200, [ - 'Content-Type' => 'text/event-stream', - 'Cache-Control' => 'no-cache', - 'Connection' => 'keep-alive', - 'X-Accel-Buffering' => 'no', - ]); - } -} \ No newline at end of file diff --git a/app/Http/Controllers/ConversationController.php b/app/Http/Controllers/ConversationController.php index ea05f0d..921f100 100644 --- a/app/Http/Controllers/ConversationController.php +++ b/app/Http/Controllers/ConversationController.php @@ -3,8 +3,6 @@ namespace App\Http\Controllers; use App\Models\ConversationSession; -use App\Models\ConversationTranscript; -use App\Models\ConversationInsight; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Inertia\Inertia; @@ -22,7 +20,7 @@ public function index() ->paginate(20); return Inertia::render('Conversations/Index', [ - 'sessions' => $sessions + 'sessions' => $sessions, ]); } @@ -64,7 +62,7 @@ public function store(Request $request) return response()->json([ 'session_id' => $session->id, - 'message' => 'Session started successfully' + 'message' => 'Session started successfully', ]); } @@ -104,7 +102,7 @@ public function end(ConversationSession $session, Request $request) ]); return response()->json([ - 'message' => 'Session ended successfully' + 'message' => 'Session ended successfully', ]); } @@ -137,7 +135,7 @@ public function saveTranscript(ConversationSession $session, Request $request) return response()->json([ 'transcript_id' => $transcript->id, - 'message' => 'Transcript saved successfully' + 'message' => 'Transcript saved successfully', ]); } @@ -174,7 +172,7 @@ public function saveBatchTranscripts(ConversationSession $session, Request $requ }); return response()->json([ - 'message' => 'Transcripts saved successfully' + 'message' => 'Transcripts saved successfully', ]); } @@ -199,7 +197,7 @@ public function saveInsight(ConversationSession $session, Request $request) return response()->json([ 'insight_id' => $insight->id, - 'message' => 'Insight saved successfully' + 'message' => 'Insight saved successfully', ]); } @@ -228,7 +226,7 @@ public function saveBatchInsights(ConversationSession $session, Request $request }); return response()->json([ - 'message' => 'Insights saved successfully' + 'message' => 'Insights saved successfully', ]); } @@ -248,7 +246,7 @@ public function updateNotes(ConversationSession $session, Request $request) ]); return response()->json([ - 'message' => 'Notes updated successfully' + 'message' => 'Notes updated successfully', ]); } @@ -268,7 +266,7 @@ public function updateTitle(ConversationSession $session, Request $request) ]); return response()->json([ - 'message' => 'Title updated successfully' + 'message' => 'Title updated successfully', ]); } @@ -284,4 +282,4 @@ public function destroy(ConversationSession $session) return redirect()->route('conversations.index') ->with('message', 'Conversation deleted successfully'); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/OnboardingController.php b/app/Http/Controllers/OnboardingController.php new file mode 100644 index 0000000..c7c17be --- /dev/null +++ b/app/Http/Controllers/OnboardingController.php @@ -0,0 +1,81 @@ +user()->has_completed_onboarding) { + return redirect()->route('dashboard'); + } + + return Inertia::render('Onboarding/Welcome'); + } + + /** + * Store the OpenAI API key and complete onboarding + */ + public function store(Request $request): RedirectResponse + { + $request->validate([ + 'openai_api_key' => ['required', 'string', 'min:20'], + ]); + + $apiKey = $request->input('openai_api_key'); + + // Validate the API key with OpenAI + try { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer '.$apiKey, + ])->get('https://api.openai.com/v1/models'); + + if (! $response->successful()) { + throw ValidationException::withMessages([ + 'openai_api_key' => ['The provided API key is invalid. Please check and try again.'], + ]); + } + } catch (\Exception $e) { + throw ValidationException::withMessages([ + 'openai_api_key' => ['Failed to validate the API key. Please ensure it\'s correct and try again.'], + ]); + } + + // Save the API key and mark onboarding as complete + $request->user()->update([ + 'openai_api_key' => $apiKey, + 'has_completed_onboarding' => true, + ]); + + return redirect()->route('dashboard')->with('success', 'Welcome! Your OpenAI API key has been saved successfully.'); + } + + /** + * Skip onboarding (user will use env variable) + */ + public function skip(Request $request): RedirectResponse + { + // Check if there's an API key in the environment + if (empty(config('openai.api_key'))) { + return back()->with('error', 'No API key found in environment. Please provide your own API key.'); + } + + // Mark onboarding as complete without storing a personal API key + $request->user()->update([ + 'has_completed_onboarding' => true, + ]); + + return redirect()->route('dashboard')->with('info', 'Using system default API key.'); + } +} diff --git a/app/Http/Controllers/RealtimeController.php b/app/Http/Controllers/RealtimeController.php index ebaf88f..424a725 100644 --- a/app/Http/Controllers/RealtimeController.php +++ b/app/Http/Controllers/RealtimeController.php @@ -3,9 +3,7 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Symfony\Component\HttpFoundation\StreamedResponse; class RealtimeController extends Controller { @@ -17,7 +15,7 @@ public function createSession(Request $request) { try { $apiKey = config('openai.api_key'); - + // For now, we'll return connection info that the frontend can use // In production, you'd want to create a proper WebSocket proxy return response()->json([ @@ -34,13 +32,14 @@ public function createSession(Request $request) 'silence_duration_ms' => 500, ], 'tools' => $this->getSalesTools(), - ] + ], ]); } catch (\Exception $e) { - Log::error('Failed to create realtime session: ' . $e->getMessage()); + Log::error('Failed to create realtime session: '.$e->getMessage()); + return response()->json([ 'status' => 'error', - 'message' => 'Failed to create session' + 'message' => 'Failed to create session', ], 500); } } @@ -52,24 +51,25 @@ public function generateEphemeralKey(Request $request) { try { $apiKey = config('openai.api_key'); - + // For now, return a mock ephemeral key structure // In production, you would call OpenAI's API to generate a real ephemeral key // Example: POST https://api.openai.com/v1/realtime/sessions - + // TODO: Implement actual ephemeral key generation when OpenAI provides the endpoint return response()->json([ 'status' => 'success', - 'ephemeralKey' => 'ek_' . bin2hex(random_bytes(32)), // Mock ephemeral key + 'ephemeralKey' => 'ek_'.bin2hex(random_bytes(32)), // Mock ephemeral key 'expiresAt' => now()->addMinutes(60)->toIso8601String(), 'model' => 'gpt-4o-realtime-preview-2024-12-17', ]); - + } catch (\Exception $e) { - Log::error('Failed to generate ephemeral key: ' . $e->getMessage()); + Log::error('Failed to generate ephemeral key: '.$e->getMessage()); + return response()->json([ 'status' => 'error', - 'message' => 'Failed to generate ephemeral key' + 'message' => 'Failed to generate ephemeral key', ], 500); } } @@ -89,20 +89,20 @@ private function getSalesTools() 'properties' => [ 'text' => [ 'type' => 'string', - 'description' => 'The complete script text to display' + 'description' => 'The complete script text to display', ], 'priority' => [ 'type' => 'string', 'enum' => ['high', 'normal', 'low'], - 'description' => 'Priority level of the script' + 'description' => 'Priority level of the script', ], 'context' => [ 'type' => 'string', - 'description' => 'Brief context about when to use this script' - ] + 'description' => 'Brief context about when to use this script', + ], ], - 'required' => ['text', 'priority', 'context'] - ] + 'required' => ['text', 'priority', 'context'], + ], ], [ 'type' => 'function', @@ -113,25 +113,25 @@ private function getSalesTools() 'properties' => [ 'talkRatio' => [ 'type' => 'number', - 'description' => 'Percentage of time salesperson is talking (0-100)' + 'description' => 'Percentage of time salesperson is talking (0-100)', ], 'sentiment' => [ 'type' => 'string', 'enum' => ['positive', 'negative', 'neutral'], - 'description' => 'Current conversation sentiment' + 'description' => 'Current conversation sentiment', ], 'topics' => [ 'type' => 'array', 'items' => ['type' => 'string'], - 'description' => 'Key topics discussed' + 'description' => 'Key topics discussed', ], 'buyingSignals' => [ 'type' => 'number', - 'description' => 'Number of buying signals detected' - ] + 'description' => 'Number of buying signals detected', + ], ], - 'required' => ['talkRatio', 'sentiment'] - ] + 'required' => ['talkRatio', 'sentiment'], + ], ], [ 'type' => 'function', @@ -143,24 +143,24 @@ private function getSalesTools() 'type' => [ 'type' => 'string', 'enum' => ['buying_signal', 'objection', 'question', 'closing_opportunity'], - 'description' => 'Type of opportunity' + 'description' => 'Type of opportunity', ], 'confidence' => [ 'type' => 'number', - 'description' => 'Confidence level (0-1)' + 'description' => 'Confidence level (0-1)', ], 'suggestion' => [ 'type' => 'string', - 'description' => 'Suggested action to take' + 'description' => 'Suggested action to take', ], 'urgency' => [ 'type' => 'string', 'enum' => ['immediate', 'soon', 'low'], - 'description' => 'How urgent is this opportunity' - ] + 'description' => 'How urgent is this opportunity', + ], ], - 'required' => ['type', 'confidence', 'suggestion'] - ] + 'required' => ['type', 'confidence', 'suggestion'], + ], ], [ 'type' => 'function', @@ -171,7 +171,7 @@ private function getSalesTools() 'properties' => [ 'topic' => [ 'type' => 'string', - 'description' => 'Topic of the battle card' + 'description' => 'Topic of the battle card', ], 'content' => [ 'type' => 'object', @@ -179,23 +179,23 @@ private function getSalesTools() 'keyPoints' => [ 'type' => 'array', 'items' => ['type' => 'string'], - 'description' => 'Key talking points' + 'description' => 'Key talking points', ], 'advantages' => [ 'type' => 'array', 'items' => ['type' => 'string'], - 'description' => 'Our advantages' + 'description' => 'Our advantages', ], 'statistics' => [ 'type' => 'array', 'items' => ['type' => 'string'], - 'description' => 'Relevant statistics' - ] - ] - ] + 'description' => 'Relevant statistics', + ], + ], + ], ], - 'required' => ['topic', 'content'] - ] + 'required' => ['topic', 'content'], + ], ], [ 'type' => 'function', @@ -206,25 +206,25 @@ private function getSalesTools() 'properties' => [ 'investment' => [ 'type' => 'number', - 'description' => 'Initial investment amount' + 'description' => 'Initial investment amount', ], 'savings' => [ 'type' => 'number', - 'description' => 'Monthly or annual savings' + 'description' => 'Monthly or annual savings', ], 'timeframe' => [ 'type' => 'string', - 'description' => 'Timeframe for ROI calculation' + 'description' => 'Timeframe for ROI calculation', ], 'additionalBenefits' => [ 'type' => 'array', 'items' => ['type' => 'string'], - 'description' => 'Non-monetary benefits' - ] + 'description' => 'Non-monetary benefits', + ], ], - 'required' => ['investment', 'savings', 'timeframe'] - ] - ] + 'required' => ['investment', 'savings', 'timeframe'], + ], + ], ]; } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Settings/ApiKeyController.php b/app/Http/Controllers/Settings/ApiKeyController.php new file mode 100644 index 0000000..d847a7a --- /dev/null +++ b/app/Http/Controllers/Settings/ApiKeyController.php @@ -0,0 +1,70 @@ +validate([ + 'openai_api_key' => ['required', 'string', 'min:20'], + ]); + + $apiKey = $request->input('openai_api_key'); + + // Validate the API key with OpenAI + try { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer '.$apiKey, + ])->get('https://api.openai.com/v1/models'); + + if (! $response->successful()) { + throw ValidationException::withMessages([ + 'openai_api_key' => ['The provided API key is invalid. Please check and try again.'], + ]); + } + } catch (\Exception $e) { + throw ValidationException::withMessages([ + 'openai_api_key' => ['Failed to validate the API key. Please ensure it\'s correct and try again.'], + ]); + } + + // Update the user's API key + $request->user()->update([ + 'openai_api_key' => $apiKey, + ]); + + return redirect()->route('api-keys.edit')->with('success', 'API key updated successfully.'); + } + + /** + * Delete the user's OpenAI API key + */ + public function destroy(Request $request): RedirectResponse + { + $request->user()->update([ + 'openai_api_key' => null, + ]); + + return redirect()->route('api-keys.edit')->with('success', 'API key deleted successfully.'); + } +} diff --git a/app/Http/Controllers/VariableController.php b/app/Http/Controllers/VariableController.php index 84e204f..e44f9be 100644 --- a/app/Http/Controllers/VariableController.php +++ b/app/Http/Controllers/VariableController.php @@ -2,16 +2,16 @@ namespace App\Http\Controllers; -use App\Services\VariableService; use App\Services\TemplateVariableResolver; -use Illuminate\Http\Request; +use App\Services\VariableService; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; use Inertia\Inertia; -use Inertia\Response; class VariableController extends Controller { private VariableService $variableService; + private TemplateVariableResolver $templateResolver; public function __construct(VariableService $variableService, TemplateVariableResolver $templateResolver) @@ -26,11 +26,11 @@ public function __construct(VariableService $variableService, TemplateVariableRe public function index(Request $request) { $category = $request->get('category'); - - $variables = $category - ? $this->variableService->getByCategory($category) + + $variables = $category + ? $this->variableService->getByCategory($category) : $this->variableService->getAll(); - + $categories = $this->variableService->getCategories(); if ($request->wantsJson()) { @@ -54,7 +54,7 @@ public function store(Request $request): JsonResponse { try { $variable = $this->variableService->upsert($request->all()); - + return response()->json([ 'message' => 'Variable created successfully', 'variable' => $variable, @@ -80,7 +80,7 @@ public function update(Request $request, string $key): JsonResponse try { $data = array_merge($request->all(), ['key' => $key]); $variable = $this->variableService->upsert($data); - + return response()->json([ 'message' => 'Variable updated successfully', 'variable' => $variable, @@ -105,13 +105,13 @@ public function destroy(string $key): JsonResponse { try { $deleted = $this->variableService->delete($key); - - if (!$deleted) { + + if (! $deleted) { return response()->json([ 'message' => 'Variable not found', ], 404); } - + return response()->json([ 'message' => 'Variable deleted successfully', ]); @@ -134,13 +134,13 @@ public function import(Request $request): JsonResponse try { $content = json_decode($request->file('file')->get(), true); - + if (json_last_error() !== JSON_ERROR_NONE) { throw new \Exception('Invalid JSON file'); } - + $this->variableService->import($content); - + return response()->json([ 'message' => 'Variables imported successfully', ]); @@ -164,9 +164,9 @@ public function export(): JsonResponse { try { $data = $this->variableService->export(); - + return response()->json($data) - ->header('Content-Disposition', 'attachment; filename="variables-export-' . now()->format('Y-m-d') . '.json"'); + ->header('Content-Disposition', 'attachment; filename="variables-export-'.now()->format('Y-m-d').'.json"'); } catch (\Exception $e) { return response()->json([ 'message' => 'Failed to export variables', @@ -190,9 +190,9 @@ public function preview(Request $request): JsonResponse $request->input('text'), $request->input('overrides', []) ); - + $extractedVariables = $this->templateResolver->extractVariables($request->input('text')); - + return response()->json([ 'original' => $request->input('text'), 'resolved' => $resolvedText, @@ -213,7 +213,7 @@ public function usage(): JsonResponse { try { $usage = $this->templateResolver->getAllUsedVariables(); - + return response()->json([ 'usage' => $usage, ]); @@ -232,7 +232,7 @@ public function seedDefaults(): JsonResponse { try { $this->variableService->seedDefaults(); - + return response()->json([ 'message' => 'Default variables seeded successfully', ]); @@ -243,4 +243,4 @@ public function seedDefaults(): JsonResponse ], 500); } } -} \ No newline at end of file +} diff --git a/app/Http/Middleware/EnsureOnboardingCompleted.php b/app/Http/Middleware/EnsureOnboardingCompleted.php new file mode 100644 index 0000000..dcc2c52 --- /dev/null +++ b/app/Http/Middleware/EnsureOnboardingCompleted.php @@ -0,0 +1,30 @@ +routeIs('onboarding.*')) { + return $next($request); + } + + // Check if user has completed onboarding + if ($request->user() && ! $request->user()->has_completed_onboarding) { + return redirect()->route('onboarding.show'); + } + + return $next($request); + } +} diff --git a/app/Models/ContextSnapshot.php b/app/Models/ContextSnapshot.php deleted file mode 100644 index 54554a5..0000000 --- a/app/Models/ContextSnapshot.php +++ /dev/null @@ -1,26 +0,0 @@ - 'array', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - ]; - - public function conversations() - { - return $this->belongsToMany(Conversation::class, 'conversation_context_snapshot'); - } -} diff --git a/app/Models/Conversation.php b/app/Models/Conversation.php deleted file mode 100644 index 932fd30..0000000 --- a/app/Models/Conversation.php +++ /dev/null @@ -1,32 +0,0 @@ - 'array', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - ]; - - public function getContextSnapshotsAttribute() - { - if (empty($this->context_snapshot_ids)) { - return collect(); - } - - return ContextSnapshot::whereIn('id', $this->context_snapshot_ids)->get(); - } -} diff --git a/app/Models/ConversationSession.php b/app/Models/ConversationSession.php index ee62168..c933eea 100644 --- a/app/Models/ConversationSession.php +++ b/app/Models/ConversationSession.php @@ -80,6 +80,7 @@ public function getDurationFormattedAttribute(): string { $minutes = floor($this->duration_seconds / 60); $seconds = $this->duration_seconds % 60; + return sprintf('%02d:%02d', $minutes, $seconds); } } diff --git a/app/Models/ConversationTranscript.php b/app/Models/ConversationTranscript.php index 0c367ad..f527c52 100644 --- a/app/Models/ConversationTranscript.php +++ b/app/Models/ConversationTranscript.php @@ -29,7 +29,7 @@ public function session(): BelongsTo public function getSpeakerLabelAttribute(): string { - return match($this->speaker) { + return match ($this->speaker) { 'salesperson' => 'You', 'customer' => 'Customer', 'system' => 'System', @@ -39,7 +39,7 @@ public function getSpeakerLabelAttribute(): string public function getSpeakerColorAttribute(): string { - return match($this->speaker) { + return match ($this->speaker) { 'salesperson' => 'text-blue-600', 'customer' => 'text-green-600', 'system' => 'text-gray-500', diff --git a/app/Models/Transcript.php b/app/Models/Transcript.php index 4945895..f3e9f69 100644 --- a/app/Models/Transcript.php +++ b/app/Models/Transcript.php @@ -57,26 +57,26 @@ public function appendTranscript(string $text): void public function appendSegment(string $text, string $speaker = 'unknown', ?float $timestamp = null): void { $segments = $this->segments ?? []; - + $segment = [ 'speaker' => $speaker, 'text' => $text, 'timestamp' => $timestamp ?? now()->timestamp, ]; - + $segments[] = $segment; $this->segments = $segments; - + // Update speaker-specific transcripts if ($speaker === 'host') { $this->host_transcript = trim(($this->host_transcript ?? '').' '.$text); } elseif ($speaker === 'guest') { $this->guest_transcript = trim(($this->guest_transcript ?? '').' '.$text); } - + // Update combined transcript with speaker labels $this->transcript = trim($this->transcript.' ['.$speaker.'] '.$text); - + $this->save(); } diff --git a/app/Models/User.php b/app/Models/User.php index 749c7b7..124a600 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -21,6 +21,8 @@ class User extends Authenticatable 'name', 'email', 'password', + 'openai_api_key', + 'has_completed_onboarding', ]; /** @@ -31,6 +33,7 @@ class User extends Authenticatable protected $hidden = [ 'password', 'remember_token', + 'openai_api_key', ]; /** @@ -43,6 +46,8 @@ protected function casts(): array return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'openai_api_key' => 'encrypted', + 'has_completed_onboarding' => 'boolean', ]; } } diff --git a/app/Models/Variable.php b/app/Models/Variable.php index ce11ebc..c80fa7b 100644 --- a/app/Models/Variable.php +++ b/app/Models/Variable.php @@ -66,7 +66,7 @@ public function validate(): bool ['value' => $this->validation_rules] ); - return !$validator->fails(); + return ! $validator->fails(); } /** @@ -92,4 +92,4 @@ public function scopeUser($query) { return $query->where('is_system', false); } -} \ No newline at end of file +} diff --git a/app/Providers/NativeAppServiceProvider.php b/app/Providers/NativeAppServiceProvider.php index 73758dd..b2657db 100644 --- a/app/Providers/NativeAppServiceProvider.php +++ b/app/Providers/NativeAppServiceProvider.php @@ -2,12 +2,12 @@ namespace App\Providers; -use Native\Laravel\Contracts\ProvidesPhpIni; +use App\Models\User; // use Native\Laravel\Facades\GlobalShortcut; -use Native\Laravel\Facades\Window; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Schema; -use App\Models\User; +use Native\Laravel\Contracts\ProvidesPhpIni; +use Native\Laravel\Facades\Window; class NativeAppServiceProvider implements ProvidesPhpIni { @@ -34,7 +34,7 @@ public function boot(): void 'contextIsolation' => false, 'webSecurity' => false, 'backgroundThrottling' => false, - 'sandbox' => false + 'sandbox' => false, ]) // Set window to floating panel level for better screen protection ->alwaysOnTop(false); diff --git a/app/Services/AIService.php b/app/Services/AIService.php deleted file mode 100644 index 9ed32e3..0000000 --- a/app/Services/AIService.php +++ /dev/null @@ -1,178 +0,0 @@ - 'Bearer ' . $apiKey, - 'Content-Type' => 'application/json', - ])->post('https://api.openai.com/v1/chat/completions', [ - 'model' => $this->defaultModel, - 'messages' => [ - ['role' => 'system', 'content' => 'You are a helpful AI assistant.'], - ['role' => 'user', 'content' => $message] - ], - 'max_tokens' => 1000, - 'temperature' => 0.7, - ]); - - if (!$response->successful()) { - throw new \Exception('OpenAI API error: ' . $response->body()); - } - - $data = $response->json(); - - return [ - 'content' => $data['choices'][0]['message']['content'] ?? 'No response', - 'model' => $this->defaultModel, - 'provider' => 'openai', - ]; - } catch (\Exception $e) { - Log::error('AI Service error: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Transcribe audio to text using OpenAI Whisper - */ - public function transcribe(string $audioData): array - { - try { - $apiKey = config('openai.api_key'); - if (empty($apiKey)) { - throw new \Exception('OpenAI API key is not configured. Please add OPENAI_API_KEY to your .env file.'); - } - - // Convert base64 audio to file - $audioContent = base64_decode($audioData); - $tempFile = tempnam(sys_get_temp_dir(), 'audio_') . '.webm'; - file_put_contents($tempFile, $audioContent); - - // Send to OpenAI Whisper API - $response = Http::withHeaders([ - 'Authorization' => 'Bearer ' . $apiKey, - ])->timeout(5)->attach( - 'file', file_get_contents($tempFile), 'audio.webm' - )->post('https://api.openai.com/v1/audio/transcriptions', [ - 'model' => 'whisper-1', - 'language' => 'en', - 'response_format' => 'text', - ]); - - // Clean up temp file - unlink($tempFile); - - if (!$response->successful()) { - throw new \Exception('Whisper API error: ' . $response->body()); - } - - // Response is plain text when using response_format: 'text' - $text = $response->body(); - - return [ - 'text' => trim($text), - ]; - } catch (\Exception $e) { - Log::error('Transcription error: ' . $e->getMessage()); - throw $e; - } - } - - /** - * Analyze sales conversation and provide coaching suggestions - */ - public function analyzeSalesConversation(string $newTranscript, string $context = ''): array - { - try { - $apiKey = config('openai.api_key'); - if (empty($apiKey)) { - throw new \Exception('OpenAI API key is not configured.'); - } - - $systemPrompt = "You are an expert sales coach AI assistant. Analyze the conversation and provide real-time coaching for the salesperson. - -For talking-points, provide COMPLETE, NATURAL SENTENCES that the salesperson can read word-for-word, like a teleprompter. Make them sound conversational and human - not robotic. Include transitions, acknowledgments, and natural flow. - -Focus on: -1. Customer objections → Provide empathetic responses with value propositions -2. Buying signals → Give closing scripts -3. Pain points → Offer discovery questions and solutions -4. Silence/lulls → Suggest engaging questions or value statements - -Example talking-point scripts: -- \"I completely understand your concern about the budget. Many of our clients felt the same way initially, but they found that the time savings alone - typically 10 hours per week - more than justified the investment.\" -- \"That's a great question about integration. Let me walk you through exactly how seamless the process is - it typically takes less than 30 minutes to set up.\" - -Return JSON format: -{ - \"suggestions\": [ - { - \"type\": \"action\"|\"talking-point\"|\"insight\", - \"priority\": \"high\"|\"normal\", - \"title\": \"Brief context (e.g., 'Budget Objection Response')\", - \"content\": \"Complete teleprompter script for talking-points, or action description\" - } - ], - \"metrics\": { - \"talkRatio\": 50, - \"sentiment\": \"positive\"|\"negative\"|\"neutral\", - \"topics\": [\"key topics discussed\"] - } -} - -Generate 1-2 high-priority talking-points max. Keep scripts concise but complete."; - - $userPrompt = "New transcript:\n$newTranscript\n\nPrevious context:\n$context\n\nAnalyze this sales conversation and provide coaching suggestions."; - - $response = Http::withHeaders([ - 'Authorization' => 'Bearer ' . $apiKey, - 'Content-Type' => 'application/json', - ])->timeout(5)->post('https://api.openai.com/v1/chat/completions', [ - 'model' => 'gpt-4o-mini', - 'messages' => [ - ['role' => 'system', 'content' => $systemPrompt], - ['role' => 'user', 'content' => $userPrompt] - ], - 'max_tokens' => 200, - 'temperature' => 0.3, - 'response_format' => ['type' => 'json_object'] - ]); - - if (!$response->successful()) { - throw new \Exception('OpenAI API error: ' . $response->body()); - } - - $data = $response->json(); - $content = $data['choices'][0]['message']['content'] ?? '{}'; - - return json_decode($content, true) ?: [ - 'suggestions' => [], - 'metrics' => null - ]; - } catch (\Exception $e) { - Log::error('Sales analysis error: ' . $e->getMessage()); - return [ - 'suggestions' => [], - 'metrics' => null - ]; - } - } -} \ No newline at end of file diff --git a/app/Services/RealtimeRelayService.php b/app/Services/RealtimeRelayService.php index 4984a8f..fb11edf 100644 --- a/app/Services/RealtimeRelayService.php +++ b/app/Services/RealtimeRelayService.php @@ -2,50 +2,65 @@ namespace App\Services; -use Ratchet\MessageComponentInterface; +use App\Models\User; +use Illuminate\Support\Facades\Log; use Ratchet\ConnectionInterface; +use Ratchet\MessageComponentInterface; use WebSocket\Client as WebSocketClient; -use Illuminate\Support\Facades\Log; class RealtimeRelayService implements MessageComponentInterface { protected $clients; + protected $openaiConnections = []; + private string $openaiRealtimeUrl = 'wss://api.openai.com/v1/realtime'; - private string $openaiApiKey; public function __construct() { $this->clients = new \SplObjectStorage; - $this->openaiApiKey = config('openai.api_key'); + } + + /** + * Get the API key - prefer user's key over env key + */ + private function getApiKey(?User $user = null): ?string + { + // Check if there's a user with an API key + if ($user && $user->openai_api_key) { + return $user->openai_api_key; + } + + // Fall back to environment variable + return config('openai.api_key'); } public function onOpen(ConnectionInterface $conn) { // Store the new connection $this->clients->attach($conn); - + // Create a connection to OpenAI for this client $this->connectToOpenAI($conn); - + Log::info("New WebSocket connection: {$conn->resourceId}"); } public function onMessage(ConnectionInterface $from, $msg) { $data = json_decode($msg, true); - + // Relay message to OpenAI if (isset($this->openaiConnections[$from->resourceId])) { $openaiConn = $this->openaiConnections[$from->resourceId]; - + // Add authentication if this is a session update if ($data['type'] === 'session.update') { $data['session']['model'] = 'gpt-4o-realtime-preview-2024-12-17'; } - + $openaiConn->send(json_encode($data)); - + Log::info("Relayed message to OpenAI from client {$from->resourceId}"); } } @@ -57,10 +72,10 @@ public function onClose(ConnectionInterface $conn) $this->openaiConnections[$conn->resourceId]->close(); unset($this->openaiConnections[$conn->resourceId]); } - + // Remove client $this->clients->detach($conn); - + Log::info("Connection {$conn->resourceId} disconnected"); } @@ -73,33 +88,40 @@ public function onError(ConnectionInterface $conn, \Exception $e) private function connectToOpenAI(ConnectionInterface $clientConn) { try { + // Get API key (for now, use the env key - in a real app, you'd pass user info) + $apiKey = $this->getApiKey(); + + if (empty($apiKey)) { + throw new \Exception('No OpenAI API key available'); + } + // Create WebSocket connection to OpenAI $headers = [ - 'Authorization' => 'Bearer ' . $this->openaiApiKey, + 'Authorization' => 'Bearer '.$apiKey, 'OpenAI-Beta' => 'realtime=v1', ]; - + $openaiConn = new WebSocketClient($this->openaiRealtimeUrl, [ 'timeout' => 60, 'headers' => $headers, ]); - + // Store the connection $this->openaiConnections[$clientConn->resourceId] = $openaiConn; - + // Set up message handler for OpenAI responses $this->setupOpenAIHandlers($clientConn, $openaiConn); - + Log::info("Connected to OpenAI Realtime API for client {$clientConn->resourceId}"); - + } catch (\Exception $e) { Log::error("Failed to connect to OpenAI: {$e->getMessage()}"); $clientConn->send(json_encode([ 'type' => 'error', 'error' => [ 'message' => 'Failed to connect to OpenAI Realtime API', - 'details' => $e->getMessage() - ] + 'details' => $e->getMessage(), + ], ])); } } @@ -109,15 +131,15 @@ private function setupOpenAIHandlers(ConnectionInterface $clientConn, WebSocketC // Start a loop to receive messages from OpenAI // This would typically run in a separate thread/process // For now, we'll handle it synchronously - + try { while ($clientConn->resourceId && isset($this->openaiConnections[$clientConn->resourceId])) { $message = $openaiConn->receive(); - + if ($message) { // Relay OpenAI response back to client $clientConn->send($message); - + Log::info("Relayed OpenAI response to client {$clientConn->resourceId}"); } } @@ -125,4 +147,4 @@ private function setupOpenAIHandlers(ConnectionInterface $clientConn, WebSocketC Log::error("Error in OpenAI message loop: {$e->getMessage()}"); } } -} \ No newline at end of file +} diff --git a/app/Services/TemplateVariableResolver.php b/app/Services/TemplateVariableResolver.php index eefca5c..be7584d 100644 --- a/app/Services/TemplateVariableResolver.php +++ b/app/Services/TemplateVariableResolver.php @@ -17,47 +17,47 @@ public function __construct(VariableService $variableService) /** * Resolve variables in a template * - * @param Template $template The template to resolve - * @param array $contextVariables Context-specific variables that override defaults + * @param Template $template The template to resolve + * @param array $contextVariables Context-specific variables that override defaults * @return array{prompt: string, variables: array, missing: array} */ public function resolve(Template $template, array $contextVariables = []): array { // Get global variables $globalVariables = $this->variableService->getVariablesAsArray(); - + // Get template-specific variables $templateVariables = $template->variables ?? []; - + // Merge variables (priority: context > template > global) $mergedVariables = array_merge($globalVariables, $templateVariables, $contextVariables); - + // Extract variable placeholders from the prompt $requiredVariables = $this->extractVariables($template->prompt); - + // Check for missing variables $missingVariables = array_diff($requiredVariables, array_keys($mergedVariables)); - + // Replace variables in the prompt $resolvedPrompt = $this->variableService->replaceInText($template->prompt, $mergedVariables); - + // Also resolve variables in talking points if they exist $resolvedTalkingPoints = []; - if (!empty($template->talking_points)) { + if (! empty($template->talking_points)) { foreach ($template->talking_points as $point) { $resolvedTalkingPoints[] = $this->variableService->replaceInText($point, $mergedVariables); } } - + // Log resolution details for debugging - if (!empty($missingVariables)) { + if (! empty($missingVariables)) { Log::warning('Missing template variables', [ 'template_id' => $template->id, 'template_name' => $template->name, 'missing' => $missingVariables, ]); } - + return [ 'prompt' => $resolvedPrompt, 'variables' => $mergedVariables, @@ -69,75 +69,68 @@ public function resolve(Template $template, array $contextVariables = []): array /** * Extract variable placeholders from text * - * @param string $text The text to extract variables from + * @param string $text The text to extract variables from * @return array List of variable names found in the text */ public function extractVariables(string $text): array { $variables = []; - + // Match {variable_name} pattern preg_match_all('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', $text, $matches); - - if (!empty($matches[1])) { + + if (! empty($matches[1])) { $variables = array_unique($matches[1]); } - + return $variables; } /** * Preview how a template would look with current variables * - * @param Template $template - * @param array $overrides Variable overrides for preview - * @return array + * @param array $overrides Variable overrides for preview */ public function preview(Template $template, array $overrides = []): array { $result = $this->resolve($template, $overrides); - + // Add additional preview information $result['preview'] = [ 'original_prompt' => $template->prompt, 'resolved_prompt' => $result['prompt'], 'used_variables' => array_intersect_key( - $result['variables'], + $result['variables'], array_flip($this->extractVariables($template->prompt)) ), 'all_available_variables' => array_keys($result['variables']), ]; - + return $result; } /** * Validate that a template has all required variables - * - * @param Template $template - * @param array $providedVariables - * @return bool */ public function validateTemplate(Template $template, array $providedVariables = []): bool { $result = $this->resolve($template, $providedVariables); + return empty($result['missing']); } /** * Get all variables used across all templates - * - * @return array */ public function getAllUsedVariables(): array { $usedVariables = []; - + $templates = Template::all(); foreach ($templates as $template) { $variables = $this->extractVariables($template->prompt); foreach ($variables as $var) { - if (!isset($usedVariables[$var])) { + if (! isset($usedVariables[$var])) { $usedVariables[$var] = [ 'variable' => $var, 'used_in' => [], @@ -149,20 +142,17 @@ public function getAllUsedVariables(): array ]; } } - + return array_values($usedVariables); } /** * Suggest variables based on template content - * - * @param string $promptText - * @return array */ public function suggestVariables(string $promptText): array { $suggestions = []; - + // Common patterns to look for $patterns = [ '/\$[\d,]+/' => 'pricing', @@ -173,13 +163,13 @@ public function suggestVariables(string $promptText): array '/\b(feature|benefit|capability)\b/i' => 'feature', '/\b(support|help|assist)\b/i' => 'support', ]; - + foreach ($patterns as $pattern => $type) { if (preg_match($pattern, $promptText)) { $suggestions[] = $type; } } - + // Get existing variables by suggested types $existingVariables = []; foreach (array_unique($suggestions) as $type) { @@ -193,7 +183,7 @@ public function suggestVariables(string $promptText): array ]; } } - + return $existingVariables; } -} \ No newline at end of file +} diff --git a/app/Services/VariableService.php b/app/Services/VariableService.php index d024ead..5a8681c 100644 --- a/app/Services/VariableService.php +++ b/app/Services/VariableService.php @@ -13,7 +13,7 @@ class VariableService * Cache key prefix for variables */ private const CACHE_PREFIX = 'variables:'; - + /** * Cache duration in seconds (1 hour) */ @@ -24,7 +24,7 @@ class VariableService */ public function getAll(): Collection { - return Cache::remember(self::CACHE_PREFIX . 'all', self::CACHE_TTL, function () { + return Cache::remember(self::CACHE_PREFIX.'all', self::CACHE_TTL, function () { return Variable::orderBy('category')->orderBy('key')->get(); }); } @@ -34,7 +34,7 @@ public function getAll(): Collection */ public function getByCategory(string $category): Collection { - return Cache::remember(self::CACHE_PREFIX . "category:{$category}", self::CACHE_TTL, function () use ($category) { + return Cache::remember(self::CACHE_PREFIX."category:{$category}", self::CACHE_TTL, function () use ($category) { return Variable::byCategory($category)->orderBy('key')->get(); }); } @@ -44,7 +44,7 @@ public function getByCategory(string $category): Collection */ public function getByKey(string $key): ?Variable { - return Cache::remember(self::CACHE_PREFIX . "key:{$key}", self::CACHE_TTL, function () use ($key) { + return Cache::remember(self::CACHE_PREFIX."key:{$key}", self::CACHE_TTL, function () use ($key) { return Variable::where('key', $key)->first(); }); } @@ -86,8 +86,8 @@ public function upsert(array $data): Variable public function delete(string $key): bool { $variable = Variable::where('key', $key)->first(); - - if (!$variable) { + + if (! $variable) { return false; } @@ -111,7 +111,7 @@ public function replaceInText(string $text, array $overrides = []): string { // Get all variables as key-value pairs $variables = $this->getVariablesAsArray(); - + // Merge with overrides (overrides take precedence) $variables = array_merge($variables, $overrides); @@ -125,7 +125,7 @@ public function replaceInText(string $text, array $overrides = []): string }; // Replace {key} pattern - $text = str_replace('{' . $key . '}', $replacement, $text); + $text = str_replace('{'.$key.'}', $replacement, $text); } return $text; @@ -137,14 +137,14 @@ public function replaceInText(string $text, array $overrides = []): string public function validate(string $key, $value): bool { $variable = $this->getByKey($key); - - if (!$variable) { + + if (! $variable) { return false; } // Set the value temporarily for validation $variable->value = $value; - + return $variable->validate(); } @@ -154,7 +154,7 @@ public function validate(string $key, $value): bool public function export(): array { $variables = Variable::all(); - + return [ 'version' => '1.0', 'exported_at' => now()->toIso8601String(), @@ -190,7 +190,7 @@ public function import(array $data): void DB::transaction(function () use ($data) { foreach ($data['variables'] as $variableData) { // Skip system variables unless explicitly allowed - if (($variableData['is_system'] ?? false) && !config('app.allow_system_variable_import', false)) { + if (($variableData['is_system'] ?? false) && ! config('app.allow_system_variable_import', false)) { continue; } @@ -207,7 +207,7 @@ public function import(array $data): void */ public function getVariablesAsArray(): array { - return Cache::remember(self::CACHE_PREFIX . 'array', self::CACHE_TTL, function () { + return Cache::remember(self::CACHE_PREFIX.'array', self::CACHE_TTL, function () { return Variable::all()->pluck('typed_value', 'key')->toArray(); }); } @@ -217,7 +217,7 @@ public function getVariablesAsArray(): array */ public function getCategories(): Collection { - return Cache::remember(self::CACHE_PREFIX . 'categories', self::CACHE_TTL, function () { + return Cache::remember(self::CACHE_PREFIX.'categories', self::CACHE_TTL, function () { return Variable::distinct('category')->pluck('category')->sort(); }); } @@ -228,13 +228,13 @@ public function getCategories(): Collection private function clearCache(?Variable $variable = null): void { // Clear all variable caches - Cache::forget(self::CACHE_PREFIX . 'all'); - Cache::forget(self::CACHE_PREFIX . 'array'); - Cache::forget(self::CACHE_PREFIX . 'categories'); - + Cache::forget(self::CACHE_PREFIX.'all'); + Cache::forget(self::CACHE_PREFIX.'array'); + Cache::forget(self::CACHE_PREFIX.'categories'); + if ($variable) { - Cache::forget(self::CACHE_PREFIX . "key:{$variable->key}"); - Cache::forget(self::CACHE_PREFIX . "category:{$variable->category}"); + Cache::forget(self::CACHE_PREFIX."key:{$variable->key}"); + Cache::forget(self::CACHE_PREFIX."category:{$variable->category}"); } } @@ -326,4 +326,4 @@ public function seedDefaults(): void ); } } -} \ No newline at end of file +} diff --git a/bootstrap/app.php b/bootstrap/app.php index ed5ad8e..56c2aad 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ validateCsrfTokens(except: [ 'api/realtime/*', ]); + + $middleware->alias([ + 'onboarding' => EnsureOnboardingCompleted::class, + ]); }) ->withExceptions(function (Exceptions $exceptions) { // diff --git a/database/migrations/2025_07_07_000045_create_conversation_sessions_table.php b/database/migrations/2025_07_07_000045_create_conversation_sessions_table.php index 60cbd6a..2553180 100644 --- a/database/migrations/2025_07_07_000045_create_conversation_sessions_table.php +++ b/database/migrations/2025_07_07_000045_create_conversation_sessions_table.php @@ -21,26 +21,26 @@ public function up(): void $table->dateTime('ended_at')->nullable(); $table->integer('duration_seconds')->default(0); $table->string('template_used')->nullable(); - + // Customer Intelligence Summary $table->string('final_intent')->nullable(); $table->string('final_buying_stage')->nullable(); $table->integer('final_engagement_level')->default(50); $table->string('final_sentiment')->nullable(); - + // Conversation Metrics $table->integer('total_transcripts')->default(0); $table->integer('total_insights')->default(0); $table->integer('total_topics')->default(0); $table->integer('total_commitments')->default(0); $table->integer('total_action_items')->default(0); - + // Summary and notes $table->text('ai_summary')->nullable(); $table->text('user_notes')->nullable(); - + $table->timestamps(); - + // Indexes $table->index(['user_id', 'started_at']); $table->index('customer_name'); diff --git a/database/migrations/2025_07_07_000049_create_conversation_transcripts_table.php b/database/migrations/2025_07_07_000049_create_conversation_transcripts_table.php index bd8ca44..aa5e0cc 100644 --- a/database/migrations/2025_07_07_000049_create_conversation_transcripts_table.php +++ b/database/migrations/2025_07_07_000049_create_conversation_transcripts_table.php @@ -21,7 +21,7 @@ public function up(): void $table->string('system_category')->nullable(); // For system messages: info, warning, success, error $table->integer('order_index'); // For maintaining order $table->timestamps(); - + // Indexes $table->index(['session_id', 'spoken_at']); $table->index(['session_id', 'order_index']); diff --git a/database/migrations/2025_07_07_000053_create_conversation_insights_table.php b/database/migrations/2025_07_07_000053_create_conversation_insights_table.php index ceb533d..929ec47 100644 --- a/database/migrations/2025_07_07_000053_create_conversation_insights_table.php +++ b/database/migrations/2025_07_07_000053_create_conversation_insights_table.php @@ -18,7 +18,7 @@ public function up(): void $table->json('data'); // Store the full object data $table->timestamp('captured_at'); $table->timestamps(); - + // Indexes $table->index(['session_id', 'insight_type']); $table->index(['session_id', 'captured_at']); diff --git a/database/migrations/2025_07_08_040023_create_variables_table.php b/database/migrations/2025_07_08_040023_create_variables_table.php index 3b6ca22..bed94af 100644 --- a/database/migrations/2025_07_08_040023_create_variables_table.php +++ b/database/migrations/2025_07_08_040023_create_variables_table.php @@ -21,7 +21,7 @@ public function up(): void $table->boolean('is_system')->default(false); $table->json('validation_rules')->nullable(); $table->timestamps(); - + // Indexes for performance $table->index('category'); $table->index('is_system'); diff --git a/database/migrations/2025_07_13_021307_add_openai_api_key_to_users_table.php b/database/migrations/2025_07_13_021307_add_openai_api_key_to_users_table.php new file mode 100644 index 0000000..2175f17 --- /dev/null +++ b/database/migrations/2025_07_13_021307_add_openai_api_key_to_users_table.php @@ -0,0 +1,29 @@ +text('openai_api_key')->nullable()->after('password'); + $table->boolean('has_completed_onboarding')->default(false)->after('openai_api_key'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['openai_api_key', 'has_completed_onboarding']); + }); + } +}; diff --git a/database/migrations/2025_07_13_153706_drop_assistant_tables.php b/database/migrations/2025_07_13_153706_drop_assistant_tables.php new file mode 100644 index 0000000..84fe1dd --- /dev/null +++ b/database/migrations/2025_07_13_153706_drop_assistant_tables.php @@ -0,0 +1,48 @@ +id(); + $table->enum('type', ['screenshot', 'audio', 'text']); + $table->longText('content')->nullable(); + $table->json('metadata')->nullable(); + $table->binary('embedding')->nullable(); + $table->timestamps(); + $table->index('type'); + $table->index('created_at'); + }); + + // Recreate conversations table + Schema::create('conversations', function (Blueprint $table) { + $table->id(); + $table->json('context_snapshot_ids')->nullable(); + $table->longText('user_message'); + $table->longText('assistant_response')->nullable(); + $table->string('model')->nullable(); + $table->string('provider')->nullable(); + $table->integer('tokens_used')->nullable(); + $table->timestamps(); + $table->index('created_at'); + }); + } +}; diff --git a/database/seeders/MeetingTemplateSeeder.php b/database/seeders/MeetingTemplateSeeder.php index 5006fd6..c4d5395 100644 --- a/database/seeders/MeetingTemplateSeeder.php +++ b/database/seeders/MeetingTemplateSeeder.php @@ -59,14 +59,14 @@ public function run(): void 'product_name' => 'Clueless', 'base_price' => '$49/user/month', 'team_price' => '$39/user/month', - 'trial_length' => '14 days' + 'trial_length' => '14 days', ], 'talking_points' => [ 'How do you currently handle meeting notes?', 'What challenges do you face with action item tracking?', 'How many meetings does your team have weekly?', 'What happens when key people miss meetings?', - 'How do you ensure nothing falls through the cracks?' + 'How do you ensure nothing falls through the cracks?', ], 'icon' => 'Phone', 'category' => 'sales_coach', @@ -119,7 +119,7 @@ public function run(): void 'variables' => [ 'demo_length' => '30 minutes', 'key_features' => '5 core features', - 'roi_multiplier' => '10x in 3 months' + 'roi_multiplier' => '10x in 3 months', ], 'talking_points' => [ 'Live transcription accuracy demonstration', @@ -127,7 +127,7 @@ public function run(): void 'Integration with existing tools', 'Security and compliance features', 'Team collaboration capabilities', - 'Analytics and insights dashboard' + 'Analytics and insights dashboard', ], 'icon' => 'Monitor', 'category' => 'sales_coach', @@ -184,7 +184,7 @@ public function run(): void 'starter_price' => '$49/user/month', 'team_price' => '$39/user/month', 'enterprise_price' => '$29/user/month', - 'annual_discount' => '20%' + 'annual_discount' => '20%', ], 'talking_points' => [ 'ROI calculation based on time saved', @@ -192,7 +192,7 @@ public function run(): void 'Volume discounts available', 'Flexible payment terms', 'Success stories from similar companies', - 'Risk-free trial period' + 'Risk-free trial period', ], 'icon' => 'DollarSign', 'category' => 'sales_coach', @@ -251,7 +251,7 @@ public function run(): void 'variables' => [ 'health_score' => 'Green/Yellow/Red', 'adoption_rate' => 'X% of licensed users', - 'expansion_potential' => 'Additional teams/features' + 'expansion_potential' => 'Additional teams/features', ], 'talking_points' => [ 'Usage metrics and adoption rates', @@ -259,7 +259,7 @@ public function run(): void 'Optimization opportunities', 'New features and updates', 'Team expansion possibilities', - 'Advanced training options' + 'Advanced training options', ], 'icon' => 'Heart', 'category' => 'customer_success', @@ -319,7 +319,7 @@ public function run(): void 'variables' => [ 'api_rate_limit' => '1000-unlimited/hour', 'setup_time' => '1 day - 6 weeks', - 'security_cert' => 'SOC2, HIPAA' + 'security_cert' => 'SOC2, HIPAA', ], 'talking_points' => [ 'API documentation walkthrough', @@ -327,7 +327,7 @@ public function run(): void 'Integration architecture', 'Data flow and storage', 'Authentication methods', - 'Sandbox environment access' + 'Sandbox environment access', ], 'icon' => 'Code', 'category' => 'technical', @@ -386,7 +386,7 @@ public function run(): void 'variables' => [ 'roi_timeline' => '3 months', 'productivity_gain' => '40%', - 'strategic_value' => 'Digital transformation enabler' + 'strategic_value' => 'Digital transformation enabler', ], 'talking_points' => [ 'Strategic vision alignment', @@ -394,7 +394,7 @@ public function run(): void 'Organization-wide impact', 'Success metrics and KPIs', 'Implementation roadmap', - 'Expected ROI and timeline' + 'Expected ROI and timeline', ], 'icon' => 'Briefcase', 'category' => 'executive', @@ -454,7 +454,7 @@ public function run(): void 'variables' => [ 'response_sla' => '24 hours', 'critical_sla' => '4 hours', - 'satisfaction_target' => '95%' + 'satisfaction_target' => '95%', ], 'talking_points' => [ 'Issue validation and reproduction', @@ -462,7 +462,7 @@ public function run(): void 'Root cause analysis', 'Prevention measures', 'Follow-up timeline', - 'Escalation if needed' + 'Escalation if needed', ], 'icon' => 'Wrench', 'category' => 'support', @@ -520,7 +520,7 @@ public function run(): void 'variables' => [ 'referral_commission' => '20% year 1', 'reseller_discount' => '30%', - 'integration_timeline' => '60-90 days' + 'integration_timeline' => '60-90 days', ], 'talking_points' => [ 'Partnership value proposition', @@ -528,7 +528,7 @@ public function run(): void 'Go-to-market strategy', 'Revenue models', 'Success metrics', - 'Next steps and timeline' + 'Next steps and timeline', ], 'icon' => 'Users', 'category' => 'partnership', @@ -587,14 +587,14 @@ public function run(): void 'variables' => [ 'meeting_duration' => '15-30 minutes', 'team_size' => 'Varies', - 'format' => 'Yesterday/Today/Blockers' + 'format' => 'Yesterday/Today/Blockers', ], 'talking_points' => [ 'What did you complete yesterday?', 'What are you working on today?', 'Any blockers or impediments?', 'Do you need help from anyone?', - 'Are we on track for sprint goals?' + 'Are we on track for sprint goals?', ], 'icon' => 'Users', 'category' => 'team_meeting', @@ -665,7 +665,7 @@ public function run(): void 'variables' => [ 'review_period' => 'Quarterly', 'planning_horizon' => 'Next 90 days', - 'key_metrics' => 'Revenue, NPS, Retention' + 'key_metrics' => 'Revenue, NPS, Retention', ], 'talking_points' => [ 'Q3 performance vs targets', @@ -675,7 +675,7 @@ public function run(): void 'Q4 priorities and goals', 'Resource requirements', 'Risk mitigation strategies', - 'Success metrics for Q4' + 'Success metrics for Q4', ], 'icon' => 'Calendar', 'category' => 'strategic', @@ -687,4 +687,4 @@ public function run(): void Template::create($template); } } -} \ No newline at end of file +} diff --git a/resources/js/components/AudioRecorder.vue b/resources/js/components/AudioRecorder.vue deleted file mode 100644 index b6d78be..0000000 --- a/resources/js/components/AudioRecorder.vue +++ /dev/null @@ -1,271 +0,0 @@ - - - - - diff --git a/resources/js/layouts/settings/Layout.vue b/resources/js/layouts/settings/Layout.vue index 0f74bb7..b074e5f 100644 --- a/resources/js/layouts/settings/Layout.vue +++ b/resources/js/layouts/settings/Layout.vue @@ -14,6 +14,10 @@ const sidebarNavItems: NavItem[] = [ title: 'Password', href: '/settings/password', }, + { + title: 'API Keys', + href: '/settings/api-keys', + }, { title: 'Appearance', href: '/settings/appearance', diff --git a/resources/js/pages/Assistant/Main.vue b/resources/js/pages/Assistant/Main.vue deleted file mode 100644 index b4914f4..0000000 --- a/resources/js/pages/Assistant/Main.vue +++ /dev/null @@ -1,591 +0,0 @@ - - - diff --git a/resources/js/pages/Dashboard.vue b/resources/js/pages/Dashboard.vue index 3737f22..bfec344 100644 --- a/resources/js/pages/Dashboard.vue +++ b/resources/js/pages/Dashboard.vue @@ -1,8 +1,10 @@ diff --git a/resources/js/pages/Onboarding/Welcome.vue b/resources/js/pages/Onboarding/Welcome.vue new file mode 100644 index 0000000..3280aca --- /dev/null +++ b/resources/js/pages/Onboarding/Welcome.vue @@ -0,0 +1,119 @@ + + + diff --git a/resources/js/pages/settings/ApiKeys.vue b/resources/js/pages/settings/ApiKeys.vue new file mode 100644 index 0000000..29c8d0e --- /dev/null +++ b/resources/js/pages/settings/ApiKeys.vue @@ -0,0 +1,155 @@ + + + diff --git a/routes/settings.php b/routes/settings.php index 98e418d..f821852 100644 --- a/routes/settings.php +++ b/routes/settings.php @@ -1,11 +1,12 @@ group(function () { +Route::middleware(['auth', 'onboarding'])->group(function () { Route::redirect('settings', '/settings/profile'); Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit'); @@ -15,6 +16,10 @@ Route::get('settings/password', [PasswordController::class, 'edit'])->name('password.edit'); Route::put('settings/password', [PasswordController::class, 'update'])->name('password.update'); + Route::get('settings/api-keys', [ApiKeyController::class, 'edit'])->name('api-keys.edit'); + Route::put('settings/api-keys', [ApiKeyController::class, 'update'])->name('api-keys.update'); + Route::delete('settings/api-keys', [ApiKeyController::class, 'destroy'])->name('api-keys.destroy'); + Route::get('settings/appearance', function () { return Inertia::render('settings/Appearance'); })->name('appearance'); diff --git a/routes/web.php b/routes/web.php index 93cf3fa..57509a9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -4,21 +4,21 @@ use Inertia\Inertia; Route::get('/', function () { - return Inertia::render('Assistant/Main', [ - 'conversations' => [], - 'flash' => session('flash', []), - ]); + return Inertia::render('Welcome'); })->name('home'); Route::get('dashboard', function () { return Inertia::render('Dashboard'); -})->middleware(['auth', 'verified'])->name('dashboard'); +})->middleware(['auth', 'verified', 'onboarding'])->name('dashboard'); -// NativePHP Desktop Routes -Route::get('/assistant', function () { - return Inertia::render('Assistant/Main'); -})->name('assistant'); +// Onboarding Routes +Route::middleware(['auth'])->group(function () { + Route::get('/onboarding', [\App\Http\Controllers\OnboardingController::class, 'show'])->name('onboarding.show'); + Route::post('/onboarding', [\App\Http\Controllers\OnboardingController::class, 'store'])->name('onboarding.store'); + Route::post('/onboarding/skip', [\App\Http\Controllers\OnboardingController::class, 'skip'])->name('onboarding.skip'); +}); +// NativePHP Desktop Routes Route::get('/realtime-agent', function () { return Inertia::render('RealtimeAgent/Main'); })->name('realtime-agent'); @@ -27,27 +27,11 @@ return Inertia::render('RealtimeAgent/Settings'); })->name('realtime-agent.settings'); -// AI Assistant API Routes -Route::post('/api/assistant/chat', [\App\Http\Controllers\AssistantController::class, 'chat']) - ->name('assistant.chat'); -Route::post('/api/assistant/transcribe', [\App\Http\Controllers\AssistantController::class, 'transcribe']) - ->name('assistant.transcribe'); -Route::post('/api/assistant/analyze-conversation', [\App\Http\Controllers\AssistantController::class, 'analyzeConversation']) - ->name('assistant.analyze-conversation'); -Route::post('/api/assistant/analyze-conversation-stream', [\App\Http\Controllers\AssistantController::class, 'analyzeConversationStream']) - ->name('assistant.analyze-conversation-stream'); - // Realtime API Routes Route::post('/api/realtime/session', [\App\Http\Controllers\RealtimeController::class, 'createSession']) ->name('realtime.session'); Route::post('/api/realtime/ephemeral-key', [\App\Http\Controllers\RealtimeController::class, 'generateEphemeralKey']) ->name('realtime.ephemeral-key'); -Route::post('/api/assistant/summarize', [\App\Http\Controllers\AssistantController::class, 'summarize']) - ->name('assistant.summarize'); -Route::post('/api/assistant/end-session', [\App\Http\Controllers\AssistantController::class, 'endSession']) - ->name('assistant.end-session'); -Route::get('/api/assistant/context-history', [\App\Http\Controllers\AssistantController::class, 'contextHistory']) - ->name('assistant.context-history'); // Template Routes Route::get('/templates', [\App\Http\Controllers\TemplateController::class, 'index']) From 41b29b808b96bb7b0233bf01eff4a80c5d30aae4 Mon Sep 17 00:00:00 2001 From: Vijay Tupakula Date: Sun, 13 Jul 2025 11:04:21 -0500 Subject: [PATCH 2/6] fix: Remove missing UI component imports and fix build errors - Removed Alert component imports that don't exist - Removed toast hook import that doesn't exist - Replaced Alert components with simple styled divs - Replaced toast notifications with browser alerts - Build now completes successfully --- config/nativephp.php | 5 ++++- resources/js/pages/Onboarding/Welcome.vue | 9 +++------ resources/js/pages/settings/ApiKeys.vue | 20 +++++--------------- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/config/nativephp.php b/config/nativephp.php index 8351195..3d3e458 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -13,7 +13,8 @@ * usually in the form of a reverse domain name. * For example: com.nativephp.app */ - 'app_id' => env('NATIVEPHP_APP_ID', 'com.nativephp.app'), + ' + ' => env('NATIVEPHP_APP_ID', 'com.nativephp.app'), /** * If your application allows deep linking, you can specify the scheme @@ -63,6 +64,8 @@ 'GITHUB_*', 'DO_SPACES_*', '*_SECRET', + 'OPENAI_API_KEY', + 'VITE_OPENAI_API_KEY', 'ZEPHPYR_*', 'NATIVEPHP_UPDATER_PATH', 'NATIVEPHP_APPLE_ID', diff --git a/resources/js/pages/Onboarding/Welcome.vue b/resources/js/pages/Onboarding/Welcome.vue index 3280aca..b16474b 100644 --- a/resources/js/pages/Onboarding/Welcome.vue +++ b/resources/js/pages/Onboarding/Welcome.vue @@ -1,5 +1,4 @@ @@ -31,11 +27,4 @@ defineProps(); - - - - - Log out - - diff --git a/resources/js/layouts/AuthLayout.vue b/resources/js/layouts/AuthLayout.vue deleted file mode 100644 index 4b09455..0000000 --- a/resources/js/layouts/AuthLayout.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/resources/js/layouts/auth/AuthCardLayout.vue b/resources/js/layouts/auth/AuthCardLayout.vue deleted file mode 100644 index 4d71ee9..0000000 --- a/resources/js/layouts/auth/AuthCardLayout.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - diff --git a/resources/js/layouts/auth/AuthSimpleLayout.vue b/resources/js/layouts/auth/AuthSimpleLayout.vue deleted file mode 100644 index a208182..0000000 --- a/resources/js/layouts/auth/AuthSimpleLayout.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/resources/js/layouts/auth/AuthSplitLayout.vue b/resources/js/layouts/auth/AuthSplitLayout.vue deleted file mode 100644 index 13e81bd..0000000 --- a/resources/js/layouts/auth/AuthSplitLayout.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - diff --git a/resources/js/layouts/settings/Layout.vue b/resources/js/layouts/settings/Layout.vue index b074e5f..08f68a6 100644 --- a/resources/js/layouts/settings/Layout.vue +++ b/resources/js/layouts/settings/Layout.vue @@ -6,14 +6,6 @@ import { type NavItem } from '@/types'; import { Link, usePage } from '@inertiajs/vue3'; const sidebarNavItems: NavItem[] = [ - { - title: 'Profile', - href: '/settings/profile', - }, - { - title: 'Password', - href: '/settings/password', - }, { title: 'API Keys', href: '/settings/api-keys', @@ -31,7 +23,7 @@ const currentPath = page.props.ziggy?.location ? new URL(page.props.ziggy.locati