Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 51 additions & 109 deletions src/aiProviders.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { AIProvider, AIResponse, AIProviderType } from './types';
import type {
AIResponse,
OpenAIProviderConfig,
CustomProviderConfig,
} from './types/ai';

type AutofillData = Record<string, string>;

Expand Down Expand Up @@ -37,13 +41,13 @@ interface AIProviderExecutor {
currentValue: string,
formContext: Record<string, any>
): Promise<AIResponse | null>;

autofill(
fields: string[],
formContext: Record<string, any>,
onProgress?: (progress: number) => void
): Promise<AutofillData | null>;

checkAvailability(): Promise<{
available: boolean;
status: string;
Expand All @@ -56,12 +60,12 @@ interface AIProviderExecutor {
*/
class ChromeAIProvider implements AIProviderExecutor {
async checkAvailability() {
if (typeof window === 'undefined' || typeof LanguageModel === 'undefined') {
if (typeof window === 'undefined' || typeof (window as any).LanguageModel === 'undefined') {
return { available: false, status: 'unavailable', needsDownload: false };
}

try {
const availability = await LanguageModel.availability();
const availability = await (window as any).LanguageModel.availability();
return {
available: availability !== 'unavailable',
status: availability,
Expand Down Expand Up @@ -93,12 +97,12 @@ Rules:

Suggested value:`;

const session = await LanguageModel.create();
const session = await (window as any).LanguageModel.create();
const result = await session.prompt(prompt);
session.destroy();

const cleaned = result.trim().replace(/^["']|["']$/g, '');
return { suggestion: cleaned, provider: 'chrome' };
return { suggestion: cleaned, provider: 'chrome-ai' }; // Updated provider key
} catch (err) {
console.error('Chrome AI error:', err);
return null;
Expand All @@ -124,9 +128,9 @@ Example format:

JSON object:`;

const session = await LanguageModel.create({
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
const session = await (window as any).LanguageModel.create({
monitor(m: any) {
m.addEventListener('downloadprogress', (e: { loaded: number }) => {
onProgress?.(e.loaded * 100);
});
},
Expand All @@ -151,7 +155,7 @@ JSON object:`;
* OpenAI Provider
*/
class OpenAIProvider implements AIProviderExecutor {
constructor(private config: Extract<AIProvider, { type: 'openai' }>) {}
constructor(private config: OpenAIProviderConfig) {}

async checkAvailability() {
return {
Expand Down Expand Up @@ -195,7 +199,7 @@ class OpenAIProvider implements AIProviderExecutor {
});

if (!response.ok) {
throw new Error(`OpenAI API error: ${response.status}`);
throw new Error(`OpenAI API error: ${response.status} - ${response.statusText}`);
}

const data = await response.json();
Expand Down Expand Up @@ -240,12 +244,12 @@ class OpenAIProvider implements AIProviderExecutor {
});

if (!response.ok) {
throw new Error(`OpenAI API error: ${response.status}`);
throw new Error(`OpenAI API error: ${response.status} - ${response.statusText}`);
}

const data = await response.json();
const content = data.choices?.[0]?.message?.content?.trim();

if (content) {
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
Expand All @@ -261,48 +265,40 @@ class OpenAIProvider implements AIProviderExecutor {
}

/**
* Custom Server Provider
* Custom Provider
*/
class CustomServerProvider implements AIProviderExecutor {
constructor(private config: Extract<AIProvider, { type: 'custom' | 'browser' }>) {}
class CustomProvider implements AIProviderExecutor {
constructor(private config: CustomProviderConfig) {}

async checkAvailability() {
try {
const response = await fetch(`${this.config.apiUrl}/health`, {
method: 'GET',
headers: this.config.headers,
});
return {
available: response.ok,
status: response.ok ? 'ready' : 'unavailable',
needsDownload: false,
};
} catch {
return { available: false, status: 'unavailable', needsDownload: false };
}
const available = typeof this.config.onCall === 'function';
return {
available,
status: available ? 'ready' : 'missing-oncall-function',
needsDownload: false,
};
}

async suggestValue(
fieldName: string,
currentValue: string,
formContext: Record<string, any>
): Promise<AIResponse | null> {
if (!this.config.onCall) return null;
try {
const response = await fetch(`${this.config.apiUrl}/api/suggest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: JSON.stringify({ fieldName, currentValue, formContext }),
const result = await this.config.onCall({
operation: 'suggest',
fieldName,
currentValue,
formContext,
});

if (!response.ok) throw new Error(`Server error: ${response.status}`);

const data = await response.json();
return data.suggestion ? { suggestion: data.suggestion, provider: 'custom' } : null;
if (typeof result === 'string') {
return { suggestion: result, provider: 'custom' };
}
console.error('Custom provider onCall for "suggest" must return a string.');
return null;
} catch (err) {
console.error('Custom server error:', err);
console.error('Custom AI provider error (suggestValue):', err);
return null;
}
}
Expand All @@ -311,75 +307,21 @@ class CustomServerProvider implements AIProviderExecutor {
fields: string[],
formContext: Record<string, any>
): Promise<AutofillData | null> {
if (!this.config.onCall) return null;
try {
const response = await fetch(`${this.config.apiUrl}/api/autofill`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: JSON.stringify({ fields, formContext }),
const result = await this.config.onCall({
operation: 'autofill',
fields,
formContext,
});

if (!response.ok) throw new Error(`Server error: ${response.status}`);

const data = await response.json();
return data.autofillData || null;
} catch (err) {
console.error('Custom server autofill error:', err);
return null;
}
}
}

/**
* Provider Factory
*/
export function createAIProvider(config: AIProvider): AIProviderExecutor {
switch (config.type) {
case 'chrome':
return new ChromeAIProvider();
case 'openai':
return new OpenAIProvider(config);
case 'custom':
case 'browser':
return new CustomServerProvider(config);
default:
throw new Error(`Unknown provider type: ${(config as any).type}`);
}
}

/**
* Execute AI providers in order with fallback
*/
export async function executeAIProviders<T>(
providers: AIProvider[],
executionOrder: AIProviderType[],
fallbackOnError: boolean,
executor: (provider: AIProviderExecutor) => Promise<T | null>
): Promise<{ result: T | null; provider: AIProviderType | null }> {
for (const providerType of executionOrder) {
const config = providers.find(p => p.type === providerType && p.enabled !== false);
if (!config) continue;

try {
const provider = createAIProvider(config);
const result = await executor(provider);

if (result !== null) {
return { result, provider: providerType };
}

if (!fallbackOnError) {
return { result: null, provider: null };
if (result && typeof result === 'object' && !Array.isArray(result)) {
return result as AutofillData;
}
console.error('Custom provider onCall for "autofill" must return an object.');
return null;
} catch (err) {
console.error(`Provider ${providerType} failed:`, err);
if (!fallbackOnError) {
return { result: null, provider: null };
}
console.error('Custom AI provider error (autofill):', err);
return null;
}
}

return { result: null, provider: null };
}
}
65 changes: 43 additions & 22 deletions src/types/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,71 @@
* AI Provider Types
*/

export type AIProviderType = 'chrome' | 'openai' | 'custom' | 'browser';
// Renamed 'chrome' to 'chrome-ai' for clarity and consistency.
export type AIProviderType = 'chrome-ai' | 'openai' | 'custom' | 'browser';

export interface AIProviderConfig {
type: AIProviderType;
enabled?: boolean;
apiKey?: string;
apiUrl?: string;
model?: string;
priority?: number;
}
// --- Individual Provider Configuration Interfaces ---
// These define the specific configurable options for each provider type.

export interface OpenAIConfig extends AIProviderConfig {
type: 'openai';
export interface OpenAIConfig {
apiKey: string;
// Added optional 'apiUrl' for custom OpenAI-compatible endpoints.
apiUrl?: string;
model?: string;
organization?: string;
}

export interface CustomServerConfig extends AIProviderConfig {
type: 'custom';
apiUrl: string;
export interface CustomProviderConfig {
apiUrl?: string;
headers?: Record<string, string>;
model?: string;
// Added hooks for custom client-side or server-side logic.
suggestValue?: (prompt: string, options?: any) => Promise<string>;
autofill?: (
schema: Record<string, any>,
data: Record<string, any>,
options?: any
) => Promise<Record<string, any>>;
}

export interface ChromeAIConfig extends AIProviderConfig {
type: 'chrome';
}
// Configuration for the built-in Chrome AI. Intentionally empty as it has no user-configurable options.
export interface ChromeAIConfig {}

export interface BrowserAIConfig extends AIProviderConfig {
type: 'browser';
export interface BrowserAIConfig {
apiUrl: string;
headers?: Record<string, string>;
model?: string;
}

// --- Comprehensive Union Type for AI Provider Configurations ---
// This is the new primary type for defining a provider and its configuration.

// Common metadata applicable to any provider instance.
interface AIProviderMetadata {
enabled?: boolean;
priority?: number;
}

export type AIProvider = OpenAIConfig | CustomServerConfig | ChromeAIConfig | BrowserAIConfig;
// A discriminated union that represents a fully configured AI provider.
// This structure is cleaner and more type-safe than the previous inheritance model.
export type AIProviderOption = AIProviderMetadata &
(
| { type: 'openai'; config: OpenAIConfig }
| { type: 'custom'; config: CustomProviderConfig }
| { type: 'chrome-ai'; config: ChromeAIConfig }
| { type: 'browser'; config: BrowserAIConfig }
);

// --- Existing types adapted to use the new structure ---

export interface AIExecutionOrder {
providers: AIProviderType[];
fallbackOnError?: boolean;
}

export interface AIFormContextValue {
providers: AIProvider[];
// Updated to use the new comprehensive AIProviderOption type.
providers: AIProviderOption[];
executionOrder: AIProviderType[];
fallbackOnError: boolean;
enabled: boolean;
Expand All @@ -57,4 +78,4 @@ export interface AIResponse {
suggestion: string;
provider: AIProviderType;
confidence?: number;
}
}