OpenAI API Key
-
- {statusInfo.text}
+
+ {status === 'configured' ? 'Configured' :
+ status === 'checking' ? 'Checking...' : 'Not configured'}
-
setOpenaiKey(e.target.value)}
- placeholder="sk-..."
- style={inputStyle}
- />
-
-
-
- Save & Test
-
-
- Clear
-
-
- setShowKey(e.target.checked)}
- style={{ marginRight: 6 }}
- />
- Show
-
+
+
+ Add your key to .env.local in the project root:
+
+
+ OPENAI_API_KEY=sk-your-key-here
+
+
+ Restart the app after changing the key.
+
@@ -182,54 +129,30 @@ const cardTitleStyle: CSSProperties = {
color: '#e5e7eb',
};
-const inputStyle: CSSProperties = {
- width: '100%',
+const instructionsStyle: CSSProperties = {
+ fontSize: 13,
+ color: '#d1d5db',
+ lineHeight: 1.5,
+};
+
+const codeInlineStyle: CSSProperties = {
+ background: 'rgba(255, 255, 255, 0.08)',
+ padding: '2px 6px',
+ borderRadius: 4,
+ fontSize: 12,
+ fontFamily: 'monospace',
+ color: '#22c55e',
+};
+
+const codeBlockStyle: CSSProperties = {
+ background: 'rgba(0, 0, 0, 0.4)',
+ border: '1px solid rgba(255, 255, 255, 0.08)',
+ borderRadius: 6,
padding: '10px 12px',
fontSize: 13,
fontFamily: 'monospace',
- background: 'rgba(0, 0, 0, 0.3)',
- border: '1px solid rgba(255, 255, 255, 0.08)',
- borderRadius: 6,
color: '#e5e7eb',
- marginBottom: 12,
- outline: 'none',
-};
-
-const buttonRowStyle: CSSProperties = {
- display: 'flex',
- gap: 8,
- alignItems: 'center',
-};
-
-const btnPrimaryStyle: CSSProperties = {
- padding: '8px 14px',
- fontSize: 12,
- fontWeight: 500,
- background: '#22c55e',
- color: '#052e16',
- border: 'none',
- borderRadius: 6,
- cursor: 'pointer',
-};
-
-const btnSecondaryStyle: CSSProperties = {
- padding: '8px 14px',
- fontSize: 12,
- fontWeight: 500,
- background: 'rgba(255, 255, 255, 0.06)',
- color: '#9ca3af',
- border: 'none',
- borderRadius: 6,
- cursor: 'pointer',
-};
-
-const toggleStyle: CSSProperties = {
- display: 'flex',
- alignItems: 'center',
- fontSize: 12,
- color: '#6b7280',
- cursor: 'pointer',
- marginLeft: 'auto',
+ marginBottom: 8,
};
const helpStyle: CSSProperties = {
diff --git a/src/components/settings/SettingsModal.tsx b/src/components/settings/SettingsModal.tsx
index 8984376..7a6297e 100644
--- a/src/components/settings/SettingsModal.tsx
+++ b/src/components/settings/SettingsModal.tsx
@@ -9,8 +9,6 @@ import DatabaseViewer from './DatabaseViewer';
import ExternalAgentsPanel from './ExternalAgentsPanel';
import ContextViewer from './ContextViewer';
import GuidesViewer from './GuidesViewer';
-import { apiKeyService } from '@/services/storage/apiKeys';
-
export type SettingsTab =
| 'logs'
| 'tools'
@@ -29,16 +27,7 @@ interface SettingsModalProps {
type TabType = SettingsTab;
export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProps) {
- // Default to API Keys tab if no keys are configured, otherwise logs
- const getDefaultTab = (): TabType => {
- if (typeof window !== 'undefined') {
- const hasKey = apiKeyService.getOpenAiKey();
- return hasKey ? 'logs' : 'apikeys';
- }
- return 'logs';
- };
-
- const [activeTab, setActiveTab] = useState
(getDefaultTab());
+ const [activeTab, setActiveTab] = useState('logs');
useEffect(() => {
if (!isOpen) return;
diff --git a/src/services/database/descriptionService.ts b/src/services/database/descriptionService.ts
index 583f803..2ec9d26 100644
--- a/src/services/database/descriptionService.ts
+++ b/src/services/database/descriptionService.ts
@@ -1,5 +1,6 @@
import { openai as openaiProvider } from '@ai-sdk/openai';
import { generateText } from 'ai';
+import { hasValidOpenAiKey } from '../storage/apiKeys';
export interface DescriptionInput {
title: string;
@@ -15,16 +16,8 @@ export interface DescriptionInput {
dimensions?: string[];
}
-/**
- * Check if we have a valid OpenAI API key configured.
- * Checks both environment variable and validates format.
- */
-export function hasValidOpenAiKey(): boolean {
- const key = process.env.OPENAI_API_KEY;
- if (!key || key === 'your-openai-api-key-here') return false;
- // Valid OpenAI keys start with sk- or sk-proj-
- return key.startsWith('sk-') && key.length > 20;
-}
+// Re-export for backwards compatibility — canonical source is ../storage/apiKeys
+export { hasValidOpenAiKey } from '../storage/apiKeys';
/**
* Generate a simple fallback description without AI.
diff --git a/src/services/database/dimensionService.ts b/src/services/database/dimensionService.ts
index 28c2d68..52c1ed3 100644
--- a/src/services/database/dimensionService.ts
+++ b/src/services/database/dimensionService.ts
@@ -1,7 +1,7 @@
import { getSQLiteClient } from './sqlite-client';
import { openai as openaiProvider } from '@ai-sdk/openai';
import { generateText } from 'ai';
-import { hasValidOpenAiKey } from './descriptionService';
+import { hasValidOpenAiKey } from '../storage/apiKeys';
export interface Dimension {
name: string;
diff --git a/src/services/database/edges.ts b/src/services/database/edges.ts
index c355772..5f1aebf 100644
--- a/src/services/database/edges.ts
+++ b/src/services/database/edges.ts
@@ -2,7 +2,6 @@ import { getSQLiteClient } from './sqlite-client';
import { Edge, EdgeContext, EdgeData, EdgeCreatedVia, NodeConnection, Node } from '@/types/database';
import { eventBroadcaster } from '../events';
import { nodeService } from './nodes';
-import { apiKeyService } from '../storage/apiKeys';
import { generateText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { z } from 'zod';
@@ -54,7 +53,7 @@ async function inferEdgeContext(params: {
// If no API key is configured, degrade gracefully.
// We still enforce explanation, but fall back to "related_to" classification.
- const apiKey = apiKeyService.getOpenAiKey();
+ const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
return { type: 'related_to', confidence: 0.0, swap_direction: false };
}
@@ -119,7 +118,7 @@ async function autoInferEdge(params: {
}): Promise<{ explanation: string; type: EdgeContext['type']; confidence: number; swap_direction: boolean }> {
const { fromNode, toNode } = params;
- const apiKey = apiKeyService.getOpenAiKey();
+ const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
// Fallback without AI
return {
diff --git a/src/services/embeddings.ts b/src/services/embeddings.ts
index 57ff86f..8129376 100644
--- a/src/services/embeddings.ts
+++ b/src/services/embeddings.ts
@@ -1,11 +1,9 @@
import OpenAI from 'openai';
-import { apiKeyService } from './storage/apiKeys';
-// Initialize OpenAI client with dynamic API key support
function getOpenAiClient(): OpenAI {
- const apiKey = apiKeyService.getOpenAiKey();
+ const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
- throw new Error('OpenAI API key required. Please:\n1. Click the Settings icon (⚙️) in the bottom left\n2. Go to API Keys tab\n3. Add your OpenAI API key\n\nGet your key at: https://platform.openai.com/api-keys');
+ throw new Error('OpenAI API key not configured. Add OPENAI_API_KEY to your .env.local file.');
}
return new OpenAI({ apiKey });
}
@@ -41,4 +39,4 @@ export class EmbeddingService {
static validateEmbedding(embedding: number[]): boolean {
return Array.isArray(embedding) && embedding.length === 1536;
}
-}
\ No newline at end of file
+}
diff --git a/src/services/storage/apiKeys.ts b/src/services/storage/apiKeys.ts
index e778650..4429f63 100644
--- a/src/services/storage/apiKeys.ts
+++ b/src/services/storage/apiKeys.ts
@@ -1,148 +1,34 @@
-// API Key Storage Service
-// Handles storage and retrieval of OpenAI API key
+// API Key helpers
+// Single source of truth: .env.local → process.env.OPENAI_API_KEY
-export interface ApiKeyStatus {
- openai: 'connected' | 'failed' | 'testing' | 'not-set';
+/**
+ * Check if a valid OpenAI API key is configured in .env.local
+ */
+export function hasValidOpenAiKey(): boolean {
+ const key = process.env.OPENAI_API_KEY;
+ if (!key || key === 'your-openai-api-key-here') return false;
+ return key.startsWith('sk-') && key.length > 20;
}
-const STORAGE_KEY = 'ra-h-api-keys';
-const FIRST_RUN_KEY = 'ra-h-first-run-complete';
-
-export class ApiKeyService {
- private static instance: ApiKeyService;
- private openaiKey: string | undefined;
- private status: ApiKeyStatus = {
- openai: 'not-set',
- };
-
- static getInstance(): ApiKeyService {
- if (!ApiKeyService.instance) {
- ApiKeyService.instance = new ApiKeyService();
- }
- return ApiKeyService.instance;
- }
-
- constructor() {
- this.loadKeys();
- }
-
- // Load keys from localStorage
- private loadKeys(): void {
- try {
- if (typeof window !== 'undefined') {
- const stored = localStorage.getItem(STORAGE_KEY);
- if (stored) {
- const parsed = JSON.parse(stored);
- this.openaiKey = parsed.openai;
- }
- }
- } catch (error) {
- console.warn('Failed to load API keys from storage:', error);
- this.openaiKey = undefined;
- }
- }
-
- // Save keys to localStorage
- private saveKeys(): void {
- try {
- if (typeof window !== 'undefined') {
- localStorage.setItem(STORAGE_KEY, JSON.stringify({ openai: this.openaiKey }));
- }
- } catch (error) {
- console.error('Failed to save API keys to storage:', error);
- }
- }
-
- // Get OpenAI API key (user key or fallback to env)
- getOpenAiKey(): string | undefined {
- // Priority: User key > Environment key
- return this.openaiKey || process.env.OPENAI_API_KEY;
- }
-
- // Set OpenAI API key
- setOpenAiKey(key: string): void {
- if (this.validateOpenAiKey(key)) {
- this.openaiKey = key;
- this.saveKeys();
- } else {
- throw new Error('Invalid OpenAI API key format');
- }
- }
-
- // Clear OpenAI key
- clearOpenAiKey(): void {
- this.openaiKey = undefined;
- this.saveKeys();
- this.status.openai = 'not-set';
- }
-
- // Get masked key for display (show only last 4 characters)
- getMaskedOpenAiKey(): string {
- if (!this.openaiKey) return '';
- return '••••••••••••••••••••' + this.openaiKey.slice(-4);
- }
-
- // Check if user has provided a key
- hasOpenAiKey(): boolean {
- return !!this.openaiKey;
- }
-
- // Validate OpenAI key format
- private validateOpenAiKey(key: string): boolean {
- return (
- typeof key === 'string' &&
- key.length > 20 &&
- (key.startsWith('sk-') || key.startsWith('sk-proj-'))
- );
- }
-
- // Test connection to OpenAI
- async testOpenAiConnection(key?: string): Promise {
- const testKey = key || this.getOpenAiKey();
- if (!testKey) return false;
-
- this.status.openai = 'testing';
-
- try {
- const response = await fetch('https://api.openai.com/v1/models', {
- headers: {
- Authorization: `Bearer ${testKey}`,
- 'Content-Type': 'application/json',
- },
- });
-
- const isConnected = response.ok;
- this.status.openai = isConnected ? 'connected' : 'failed';
- return isConnected;
- } catch (error) {
- console.error('OpenAI connection test failed:', error);
- this.status.openai = 'failed';
- return false;
- }
- }
-
- // Get connection status
- getStatus(): ApiKeyStatus {
- return { ...this.status };
- }
-
- // Update status
- updateStatus(status: ApiKeyStatus['openai']): void {
- this.status.openai = status;
- }
-
- // First-run tracking
- isFirstRun(): boolean {
- if (typeof window === 'undefined') return false;
- return !localStorage.getItem(FIRST_RUN_KEY);
- }
-
- markFirstRunComplete(): void {
- if (typeof window !== 'undefined') {
- localStorage.setItem(FIRST_RUN_KEY, 'true');
- }
- }
+/**
+ * Get the OpenAI API key from environment
+ */
+export function getOpenAiKey(): string | undefined {
+ const key = process.env.OPENAI_API_KEY;
+ if (!key || key === 'your-openai-api-key-here') return undefined;
+ return key;
}
-// Export singleton instance
-export const apiKeyService = ApiKeyService.getInstance();
+/**
+ * Check if first run (no API key configured)
+ */
+export function isFirstRun(): boolean {
+ if (typeof window === 'undefined') return false;
+ return !localStorage.getItem('ra-h-first-run-complete');
+}
+
+export function markFirstRunComplete(): void {
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('ra-h-first-run-complete', 'true');
+ }
+}