Initial commit from ux_aura_assistant
This commit is contained in:
160
services/apiService.ts
Normal file
160
services/apiService.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
|
||||
import { getUrlWithStudioAuth, getFetchOptions } from './apiUtils';
|
||||
|
||||
const getApiBaseUrl = (): string => {
|
||||
const isStudioMode = window.location.href.includes('.goog');
|
||||
|
||||
if (isStudioMode) {
|
||||
// In Studio Mode, use the absolute URL for the dev environment.
|
||||
return 'https://www.playtest.humanizeiq.ai/api/r2-explorer';
|
||||
}
|
||||
|
||||
// In deployed environments (dev, prod, local), use a relative URL.
|
||||
return '/api/r2-explorer';
|
||||
};
|
||||
|
||||
export async function uploadFile(file: File, metadata: object): Promise<any> {
|
||||
const baseUrl = `${getApiBaseUrl()}/upload`;
|
||||
const url = await getUrlWithStudioAuth(baseUrl);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('metadata', JSON.stringify(metadata));
|
||||
|
||||
const options = await getFetchOptions({
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
const error: any = new Error(`File upload failed with status ${response.status}: ${errorText}`);
|
||||
try {
|
||||
error.body = JSON.parse(errorText);
|
||||
} catch (e) {
|
||||
error.body = { error: errorText };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateFile(file: File, fileIdOrPath: string, isPath: boolean = false, metadata?: object): Promise<any> {
|
||||
const baseUrl = `${getApiBaseUrl()}/update-file`;
|
||||
const url = await getUrlWithStudioAuth(baseUrl);
|
||||
|
||||
// Construct query params carefully to preserve existing auth params
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
const paramName = isPath ? 'path' : 'fileId';
|
||||
const fetchUrl = `${url}${separator}${paramName}=${encodeURIComponent(fileIdOrPath)}`;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (metadata) {
|
||||
formData.append('metadata', JSON.stringify(metadata));
|
||||
}
|
||||
|
||||
const options = await getFetchOptions({
|
||||
method: 'PUT',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(fetchUrl, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
const error: any = new Error(`File update failed with status ${response.status}: ${errorText}`);
|
||||
try {
|
||||
error.body = JSON.parse(errorText);
|
||||
} catch (e) {
|
||||
error.body = { error: errorText };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error updating file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function listFiles(category?: string): Promise<any> {
|
||||
const baseUrl = `${getApiBaseUrl()}/files`;
|
||||
let url = await getUrlWithStudioAuth(baseUrl);
|
||||
|
||||
if (category) {
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
url = `${url}${separator}category=${encodeURIComponent(category)}`;
|
||||
}
|
||||
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to list files: ${response.status} ${errorText}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function listFilesByMetadata(metadata: any): Promise<any> {
|
||||
const baseUrl = `${getApiBaseUrl()}/files-by-metadata`;
|
||||
const url = await getUrlWithStudioAuth(baseUrl);
|
||||
|
||||
const options = await getFetchOptions({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ metadata })
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to list files by metadata: ${response.status} ${errorText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error listing files by metadata:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFile(pathOrId: string, isPath: boolean = true): Promise<any> {
|
||||
const baseUrl = `${getApiBaseUrl()}/download-file`;
|
||||
let url = await getUrlWithStudioAuth(baseUrl);
|
||||
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
const param = isPath ? 'path' : 'fileId';
|
||||
url = `${url}${separator}${param}=${encodeURIComponent(pathOrId)}`;
|
||||
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to download file: ${response.status} ${errorText}`);
|
||||
}
|
||||
// Return JSON directly as we are storing JSON files
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function deleteFile(pathOrId: string, isPath: boolean = true): Promise<any> {
|
||||
const baseUrl = `${getApiBaseUrl()}/delete-file`;
|
||||
let url = await getUrlWithStudioAuth(baseUrl);
|
||||
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
const param = isPath ? 'path' : 'fileId';
|
||||
url = `${url}${separator}${param}=${encodeURIComponent(pathOrId)}`;
|
||||
|
||||
const options = await getFetchOptions({ method: 'DELETE' });
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to delete file: ${response.status} ${errorText}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
87
services/apiUtils.ts
Normal file
87
services/apiUtils.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
|
||||
/**
|
||||
* Shared utility functions for making authenticated API calls.
|
||||
* Handles logic for both Studio Mode and Deployed Mode.
|
||||
*/
|
||||
|
||||
export const isStudioMode = (): boolean => {
|
||||
const hostname = window.location.href;
|
||||
return hostname.includes('.goog');
|
||||
};
|
||||
|
||||
// --- Studio Mode Cookie Loading (Async) ---
|
||||
// This promise ensures the cookie is fetched only once per session.
|
||||
let studioCookiePromise: Promise<string | null> | null = null;
|
||||
|
||||
export const fetchStudioCookie = (): Promise<string | null> => {
|
||||
if (!isStudioMode()) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (studioCookiePromise) {
|
||||
return studioCookiePromise;
|
||||
}
|
||||
studioCookiePromise = (async () => {
|
||||
try {
|
||||
// Fetch from the application's relative path
|
||||
const response = await fetch('./local_cookie.json');
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to fetch local_cookie.json: ${response.statusText}`);
|
||||
return null;
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data && typeof data.cookie === 'string') {
|
||||
return data.cookie;
|
||||
}
|
||||
console.error("Invalid format for local_cookie.json. Expected { \"cookie\": \"...\" }");
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching or parsing local_cookie.json:", error);
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
return studioCookiePromise;
|
||||
};
|
||||
// --- End Studio Mode Cookie Loading ---
|
||||
|
||||
/**
|
||||
* Appends the Studio Mode authentication cookie as a query parameter to a URL if needed.
|
||||
* @param baseUrl The base URL for the API call.
|
||||
* @returns The URL with the auth parameter if in Studio Mode.
|
||||
*/
|
||||
export const getUrlWithStudioAuth = async (baseUrl: string): Promise<string> => {
|
||||
if (!isStudioMode()) {
|
||||
return baseUrl;
|
||||
}
|
||||
const cookie = await fetchStudioCookie();
|
||||
if (!cookie) {
|
||||
return baseUrl;
|
||||
}
|
||||
const param = `X-Studio-Cookie=${encodeURIComponent(cookie)}`;
|
||||
if (baseUrl.includes('?')) {
|
||||
return `${baseUrl}&${param}`;
|
||||
} else {
|
||||
return `${baseUrl}?${param}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the options object for a fetch call, including credentials and headers.
|
||||
* @param options Initial RequestInit options.
|
||||
* @returns A complete RequestInit object for the fetch call.
|
||||
*/
|
||||
export const getFetchOptions = async (options: RequestInit = {}): Promise<RequestInit> => {
|
||||
const headers = new Headers(options.headers);
|
||||
|
||||
// For FormData, let the browser set the Content-Type with the correct boundary.
|
||||
// For other POST/PUT/PATCH requests, default to application/json if not set.
|
||||
if (!(options.body instanceof FormData) && options.method && ['POST', 'PUT', 'PATCH'].includes(options.method.toUpperCase()) && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
// Studio auth is handled via query parameter, but 'credentials: include' is still needed for deployed mode cookies.
|
||||
return {
|
||||
...options,
|
||||
credentials: 'include',
|
||||
headers: headers,
|
||||
};
|
||||
};
|
||||
122
services/appBuilder/componentService.ts
Normal file
122
services/appBuilder/componentService.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
|
||||
import type { ProjectComponent } from '../../types';
|
||||
import { isStudioMode, getUrlWithStudioAuth, getFetchOptions } from '../apiUtils';
|
||||
import { getAppBuilderApiBaseUrl } from './config';
|
||||
|
||||
// Helper to resolve the App Builder's own component based on metadata.json
|
||||
let selfComponentPromise: Promise<ProjectComponent | null> | null = null;
|
||||
|
||||
// Re-implementing simplified getOrganizations/getProjects/getComponents locally to avoid circular dependencies or importing unused modules
|
||||
// purely for getSelfComponent resolution logic if needed, or we can assume metadata is correct.
|
||||
// However, the original implementation relied on fetching from DB to confirm.
|
||||
// We will keep minimal fetch logic for getSelfComponent resolution.
|
||||
|
||||
const fetchComponentsMinimal = async (projectId: number): Promise<ProjectComponent[]> => {
|
||||
const baseUrl = `${getAppBuilderApiBaseUrl()}/app-builder/components`;
|
||||
const urlWithParams = new URL(baseUrl, window.location.origin);
|
||||
urlWithParams.searchParams.append('projectId', projectId.toString());
|
||||
const finalUrl = baseUrl.startsWith('http') ? urlWithParams.href : `${urlWithParams.pathname}${urlWithParams.search}`;
|
||||
const url = await getUrlWithStudioAuth(finalUrl);
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json();
|
||||
return Array.isArray(data) ? data.map((c: any) => ({...c, projectId: c.projectId || c.project_id})) : [];
|
||||
};
|
||||
|
||||
const fetchOrganizationsMinimal = async (): Promise<any[]> => {
|
||||
const baseUrl = `${getAppBuilderApiBaseUrl()}/app-builder/organizations`;
|
||||
const url = await getUrlWithStudioAuth(baseUrl);
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
const response = await fetch(url, options);
|
||||
return response.ok ? await response.json() : [];
|
||||
};
|
||||
|
||||
const fetchProjectsMinimal = async (orgId: number): Promise<any[]> => {
|
||||
const baseUrl = `${getAppBuilderApiBaseUrl()}/app-builder/projects`;
|
||||
const urlWithParams = new URL(baseUrl, window.location.origin);
|
||||
urlWithParams.searchParams.append('organizationId', orgId.toString());
|
||||
const finalUrl = baseUrl.startsWith('http') ? urlWithParams.href : `${urlWithParams.pathname}${urlWithParams.search}`;
|
||||
const url = await getUrlWithStudioAuth(finalUrl);
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
const response = await fetch(url, options);
|
||||
return response.ok ? await response.json() : [];
|
||||
};
|
||||
|
||||
export const getSelfComponent = async (): Promise<ProjectComponent | null> => {
|
||||
if (selfComponentPromise) return selfComponentPromise;
|
||||
|
||||
selfComponentPromise = (async () => {
|
||||
try {
|
||||
console.log("getSelfComponent: Starting lookup...");
|
||||
let url: string;
|
||||
// Use document.baseURI to correctly locate metadata.json in both Studio and Deployed modes
|
||||
if (isStudioMode()) {
|
||||
const base = document.baseURI.endsWith('/') ? document.baseURI : `${document.baseURI}/`;
|
||||
url = new URL('metadata.json', base).href;
|
||||
} else {
|
||||
// Calculate base: Host + first path segment (ignoring index.html)
|
||||
const pathParts = window.location.pathname.split('/').filter(p => p && p !== 'index.html');
|
||||
const base = pathParts.length > 0
|
||||
? `${window.location.origin}/${pathParts[0]}/`
|
||||
: `${window.location.origin}/`;
|
||||
url = new URL('metadata.json', base).href;
|
||||
}
|
||||
|
||||
const metaRes = await fetch(url);
|
||||
|
||||
if (!metaRes.ok) {
|
||||
console.warn(`getSelfComponent: Could not fetch metadata.json. Status: ${metaRes.status}`);
|
||||
return null;
|
||||
}
|
||||
const metadata = await metaRes.json();
|
||||
console.log("getSelfComponent: Metadata loaded:", metadata);
|
||||
|
||||
const { organization, project, component } = metadata;
|
||||
|
||||
if (!organization || !project || !component) {
|
||||
console.warn("getSelfComponent: metadata.json missing required fields (organization, project, component)");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. Find Org
|
||||
const orgs = await fetchOrganizationsMinimal();
|
||||
const orgObj = orgs.find(o => o.name === organization);
|
||||
if (!orgObj) {
|
||||
console.warn(`getSelfComponent: Organization '${organization}' not found in DB.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. Find Project
|
||||
const projects = await fetchProjectsMinimal(orgObj.id);
|
||||
const projObj = projects.find(p => p.name === project);
|
||||
if (!projObj) {
|
||||
console.warn(`getSelfComponent: Project '${project}' not found in org '${organization}'.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. Find Component
|
||||
const components = await fetchComponentsMinimal(projObj.id);
|
||||
// Match by name or title
|
||||
const compObj = components.find(c => c.name === component || c.title === component);
|
||||
if (!compObj) {
|
||||
console.warn(`getSelfComponent: Component '${component}' not found in project '${project}'.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`getSelfComponent: Resolved Self Component: ${compObj.id} for ${organization}/${project}/${component}`);
|
||||
return compObj;
|
||||
|
||||
} catch (e) {
|
||||
console.error("getSelfComponent: Failed to resolve self component", e);
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
return selfComponentPromise;
|
||||
};
|
||||
|
||||
export const getSelfComponentId = async (): Promise<number | null> => {
|
||||
const comp = await getSelfComponent();
|
||||
return comp ? comp.id : null;
|
||||
};
|
||||
21
services/appBuilder/config.ts
Normal file
21
services/appBuilder/config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
export const getAppBuilderApiBaseUrl = (): string => {
|
||||
const isStudioMode = window.location.href.includes('.goog');
|
||||
if (isStudioMode) {
|
||||
// Management APIs (RBAC, Auth) in the HumanizeIQ ecosystem usually sit under this segment
|
||||
return 'https://www.playtest.humanizeiq.ai/api/ai_studio_manager_api';
|
||||
} else {
|
||||
// In deployed mode, use a relative path.
|
||||
return '/api/ai_studio_manager_api';
|
||||
}
|
||||
};
|
||||
export const getUserManagementApiBaseUrl = (): string => {
|
||||
const isStudioMode = window.location.href.includes('.goog');
|
||||
if (isStudioMode) {
|
||||
// Management APIs (RBAC, Auth) in the HumanizeIQ ecosystem usually sit under this segment
|
||||
return 'https://www.playtest.humanizeiq.ai/api/user_management';
|
||||
} else {
|
||||
// In deployed mode, use a relative path.
|
||||
return '/api/user_management';
|
||||
}
|
||||
};
|
||||
1
services/appBuilder/contentService.ts
Normal file
1
services/appBuilder/contentService.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {};
|
||||
84
services/appBuilder/promptService.ts
Normal file
84
services/appBuilder/promptService.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
import type { ComponentPrompt } from '../../types';
|
||||
import { getUrlWithStudioAuth, getFetchOptions } from '../apiUtils';
|
||||
import { getAppBuilderApiBaseUrl } from './config';
|
||||
import { getSelfComponentId } from './componentService';
|
||||
|
||||
/**
|
||||
* Retrieves prompts for a specific component.
|
||||
* @param componentId The ID of the component.
|
||||
*/
|
||||
export const getComponentPrompts = async (componentId: number): Promise<ComponentPrompt[]> => {
|
||||
console.log(`API: Fetching prompts for componentId ${componentId}`);
|
||||
const baseUrl = `${getAppBuilderApiBaseUrl()}/app-builder/component-prompts`;
|
||||
|
||||
const urlWithParams = new URL(baseUrl, window.location.origin);
|
||||
urlWithParams.searchParams.append('componentId', componentId.toString());
|
||||
const finalUrl = baseUrl.startsWith('http') ? urlWithParams.href : `${urlWithParams.pathname}${urlWithParams.search}`;
|
||||
|
||||
const url = await getUrlWithStudioAuth(finalUrl);
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
const error: any = new Error(`API call failed with status ${response.status}: ${errorText}`);
|
||||
error.status = response.status;
|
||||
try { error.body = JSON.parse(errorText); } catch (e) { error.body = errorText; }
|
||||
throw error;
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
console.error(`Error fetching prompts for component ${componentId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to convert prompt array to record map
|
||||
*/
|
||||
const convertPromptsToMap = (prompts: ComponentPrompt[]): Record<string, string> => {
|
||||
const map: Record<string, string> = {};
|
||||
if (Array.isArray(prompts)) {
|
||||
prompts.forEach(p => {
|
||||
if (p && p.title) {
|
||||
map[p.title] = p.content;
|
||||
}
|
||||
});
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves prompts for a specific component as a key-value map.
|
||||
* This is useful for easy lookup by prompt title.
|
||||
*/
|
||||
export const getPrompts = async (componentId: number): Promise<Record<string, string>> => {
|
||||
try {
|
||||
const prompts = await getComponentPrompts(componentId);
|
||||
return convertPromptsToMap(prompts);
|
||||
} catch (e) {
|
||||
console.error(`Failed to get prompts for component ${componentId}`, e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves system prompts for the current application (Self Component).
|
||||
* Returns a map of prompt title to prompt content.
|
||||
*/
|
||||
export const getSystemPrompts = async (): Promise<Record<string, string>> => {
|
||||
try {
|
||||
const selfId = await getSelfComponentId();
|
||||
if (!selfId) {
|
||||
console.warn("getSystemPrompts: No self component ID found.");
|
||||
return {};
|
||||
}
|
||||
const prompts = await getComponentPrompts(selfId);
|
||||
return convertPromptsToMap(prompts);
|
||||
} catch (e) {
|
||||
console.error("Failed to get system prompts", e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
31
services/appBuilder/rbacService.ts
Normal file
31
services/appBuilder/rbacService.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
import type { Role, Permission } from '../../types';
|
||||
import { getUrlWithStudioAuth, getFetchOptions } from '../apiUtils';
|
||||
import { getAppBuilderApiBaseUrl } from './config';
|
||||
|
||||
/**
|
||||
* Retrieves the current user's roles and permissions from the RBAC endpoint.
|
||||
* @returns A promise that resolves with an object containing arrays of roles and permissions.
|
||||
*/
|
||||
export const getMyRbacDetails = async (): Promise<{ roles: Role[], permissions: Permission[] }> => {
|
||||
console.log('API: Fetching RBAC details (roles and permissions)');
|
||||
const baseUrl = `${getAppBuilderApiBaseUrl()}/rbac/me`;
|
||||
const url = await getUrlWithStudioAuth(baseUrl);
|
||||
const options = await getFetchOptions({ method: 'GET' });
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to fetch RBAC details: ${response.status} ${errorText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return {
|
||||
roles: Array.isArray(data.roles) ? data.roles : [],
|
||||
permissions: Array.isArray(data.permissions) ? data.permissions : []
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching user RBAC details:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
8
services/appBuilderService.ts
Normal file
8
services/appBuilderService.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
// This file re-exports all functionalities from the modular services.
|
||||
// This maintains backward compatibility with existing imports.
|
||||
|
||||
export * from './appBuilder/config';
|
||||
export * from './appBuilder/componentService';
|
||||
export * from './appBuilder/promptService';
|
||||
export * from './appBuilder/rbacService';
|
||||
54
services/geminiService.ts
Normal file
54
services/geminiService.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import { getSystemPrompts } from './appBuilderService';
|
||||
import { fetchStudioCookie } from './apiUtils'
|
||||
|
||||
/**
|
||||
* Returns a configured GoogleGenAI instance.
|
||||
* Per @google/genai guidelines, it strictly uses process.env.API_KEY.
|
||||
*/
|
||||
export const getAi = async () => {
|
||||
// The API key must be obtained exclusively from process.env.API_KEY
|
||||
const apiKey = process.env.API_KEY || 'NOT_FOUND';
|
||||
|
||||
const href = window.location.href;
|
||||
const hostname = window.location.hostname;
|
||||
const isStudioMode = href.includes('.goog');
|
||||
const isLocal = hostname === 'localhost' || hostname === '127.0.0.1';
|
||||
|
||||
/**
|
||||
* HumanizeIQ specific: Non-studio modes (deployed apps) route through a proxy
|
||||
* for unified access control.
|
||||
*/
|
||||
let baseUrl='https://www.playtest.humanizeiq.ai/api-proxy'
|
||||
if (!isStudioMode) {
|
||||
baseUrl = `${window.location.origin}/api-proxy`;
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
'User-Agent': 'DraftingStudio'
|
||||
};
|
||||
if (isStudioMode) {
|
||||
const cookie = await fetchStudioCookie();
|
||||
if (cookie) {
|
||||
headers['X-Studio-Cookie'] = cookie;
|
||||
}
|
||||
}
|
||||
|
||||
return new GoogleGenAI({
|
||||
apiKey: apiKey,
|
||||
httpOptions: {
|
||||
baseUrl: baseUrl,
|
||||
headers: headers
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Cache for system prompts to avoid repeated fetches
|
||||
let systemPromptsCache: Record<string, string> | null = null;
|
||||
|
||||
export const getSystemInstruction = async (key: string): Promise<string> => {
|
||||
if (!systemPromptsCache) {
|
||||
systemPromptsCache = await getSystemPrompts();
|
||||
}
|
||||
return systemPromptsCache[key] || '';
|
||||
};
|
||||
89
services/llmService.ts
Normal file
89
services/llmService.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
|
||||
|
||||
import { getAi } from './geminiService';
|
||||
import { isStudioMode, fetchStudioCookie } from './apiUtils';
|
||||
import OpenAI from 'openai';
|
||||
import type { ChatMessage, ModelProvider, LLMConfig } from '../types';
|
||||
|
||||
export const MODELS: Record<ModelProvider, string[]> = {
|
||||
openai: ['chatgpt-latest'],
|
||||
// Use the latest recommended model for basic text tasks
|
||||
google: ['gemini-3-flash-preview']
|
||||
};
|
||||
|
||||
export const generateResponse = async (
|
||||
config: LLMConfig,
|
||||
messages: ChatMessage[]
|
||||
): Promise<string> => {
|
||||
// Prepare headers for authentication (needed for proxy in Studio Mode)
|
||||
const headers: Record<string, string> = {};
|
||||
if (isStudioMode()) {
|
||||
const cookie = await fetchStudioCookie();
|
||||
if (cookie) {
|
||||
headers['X-Studio-Cookie'] = cookie;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.provider === 'google') {
|
||||
const ai = await getAi();
|
||||
const history = messages.slice(0, -1).map(m => ({
|
||||
role: m.role,
|
||||
parts: [{ text: m.context || m.content }]
|
||||
}));
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const content = lastMessage.context || lastMessage.content;
|
||||
|
||||
const chatConfig: any = {
|
||||
model: config.model,
|
||||
history: history,
|
||||
};
|
||||
|
||||
if (config.systemInstruction) {
|
||||
chatConfig.config = {
|
||||
systemInstruction: config.systemInstruction
|
||||
};
|
||||
}
|
||||
|
||||
const chat = ai.chats.create(chatConfig);
|
||||
|
||||
const response = await chat.sendMessage({ message: content });
|
||||
// Correct usage of .text property
|
||||
return response.text || "No response text.";
|
||||
|
||||
} else if (config.provider === 'openai') {
|
||||
// API Key handled by proxy
|
||||
const apiKey = config.apiKey || 'managed-by-proxy';
|
||||
|
||||
const baseURL = 'https://www.playtest.humanizeiq.ai/api-proxy/openai/v1';
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: apiKey,
|
||||
baseURL: baseURL,
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: headers
|
||||
});
|
||||
|
||||
let openAiMessages = messages.map(m => ({
|
||||
role: m.role === 'model' ? 'assistant' : 'user',
|
||||
content: m.context || m.content
|
||||
})) as any[];
|
||||
|
||||
if (config.systemInstruction) {
|
||||
openAiMessages = [
|
||||
{ role: 'system', content: config.systemInstruction },
|
||||
...openAiMessages
|
||||
];
|
||||
}
|
||||
|
||||
const completion = await openai.chat.completions.create({
|
||||
messages: openAiMessages,
|
||||
model: config.model,
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content || "No response text.";
|
||||
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported provider: ${config.provider}`);
|
||||
};
|
||||
Reference in New Issue
Block a user