File size: 4,020 Bytes
964103b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6aa1546
964103b
 
 
 
 
 
 
6aa1546
964103b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
 * API client for Smart Escalation API
 * Handles communication with the backend /ask endpoint
 */

// API Response Types
export interface ApiResponse {
  response_type: 'answer' | 'escalation';
  message: string;
  confidence_explanation: string;
  sources: string[] | null;
}

// API Request Types
export interface QuestionRequest {
  question: string;
}

// Error Types
export class ApiError extends Error {
  statusCode?: number;
  originalError?: unknown;

  constructor(
    message: string,
    statusCode?: number,
    originalError?: unknown
  ) {
    super(message);
    this.name = 'ApiError';
    this.statusCode = statusCode;
    this.originalError = originalError;
  }
}

/**
 * Get API URL from environment variables
 * For Vercel deployment, uses relative path /api
 * For local development, uses localhost:8000
 */
const getApiUrl = (): string => {
  // In production (Vercel), use relative path to serverless function
  // In development, use environment variable or default to localhost
  return import.meta.env.VITE_API_URL || '/api';
};

/**
 * POST a question to the API endpoint
 * 
 * @param question - The customer question to submit
 * @returns Promise resolving to the API response
 * @throws ApiError on network errors, timeouts, or invalid responses
 */
export const askQuestion = async (question: string): Promise<ApiResponse> => {
  const apiUrl = getApiUrl();
  const endpoint = apiUrl; // Use /api directly (not /api/ask)

  // Validate input
  if (!question || question.trim().length === 0) {
    throw new ApiError('Question cannot be empty', 400);
  }

  const requestBody: QuestionRequest = {
    question: question.trim()
  };

  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout

    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(requestBody),
      signal: controller.signal,
    });

    clearTimeout(timeoutId);

    // Handle HTTP errors
    if (!response.ok) {
      let errorMessage = `API request failed with status ${response.status}`;
      
      try {
        const errorData = await response.json();
        errorMessage = errorData.detail || errorData.message || errorMessage;
      } catch {
        // If error response is not JSON, use status text
        errorMessage = response.statusText || errorMessage;
      }

      throw new ApiError(errorMessage, response.status);
    }

    // Parse response
    const data = await response.json();

    // Validate response structure
    if (!isValidApiResponse(data)) {
      throw new ApiError('Invalid response format from API', 500);
    }

    return data;

  } catch (error) {
    // Handle network errors
    if (error instanceof ApiError) {
      throw error;
    }

    if (error instanceof Error) {
      if (error.name === 'AbortError') {
        throw new ApiError('Request timeout - please try again', 408, error);
      }
      
      if (error.message.includes('fetch')) {
        throw new ApiError(
          'Unable to connect to the API. Please check your connection and try again.',
          0,
          error
        );
      }

      throw new ApiError(
        `Network error: ${error.message}`,
        0,
        error
      );
    }

    throw new ApiError('An unexpected error occurred', 0, error);
  }
};

/**
 * Type guard to validate API response structure
 */
function isValidApiResponse(data: unknown): data is ApiResponse {
  if (typeof data !== 'object' || data === null) {
    return false;
  }

  const response = data as Record<string, unknown>;

  return (
    typeof response.response_type === 'string' &&
    (response.response_type === 'answer' || response.response_type === 'escalation') &&
    typeof response.message === 'string' &&
    typeof response.confidence_explanation === 'string' &&
    (response.sources === null || Array.isArray(response.sources))
  );
}