Add Next.js website with GitHub Pages deployment

- Added complete Next.js single-page website for Vocalinux
- Configured static export with proper GitHub Pages deployment
- Added GitHub Actions workflow for automated deployment to gh-pages branch
- Configured custom domain (vocalinux.com) with CNAME file
- Added comprehensive build script with npm support
- Responsive design with dark/light theme toggle
- Modern UI components with Tailwind CSS and shadcn/ui
- Proper SEO metadata and accessibility features
This commit is contained in:
jatinkrmalik
2025-05-25 21:11:17 -05:00
parent 27471242c5
commit ec68dd90f0
26 changed files with 2592 additions and 5 deletions
+594
View File
@@ -0,0 +1,594 @@
export class AIServiceError extends Error {
statusCode: number;
provider: string;
constructor(message: string, statusCode: number, provider: string) {
super(message);
this.name = 'AIServiceError';
this.statusCode = statusCode;
this.provider = provider;
}
}
export type TextGenerationResult = {
text: string;
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
finishReason?: 'stop' | 'length' | 'content_filter';
};
// Vision-enabled Text Types
export type ContentItem =
| { type: 'text'; text: string }
| { type: 'image_url'; image_url: { url: string } };
export type Message = {
role: 'user' | 'assistant' | 'system';
content: ContentItem[];
};
// We reuse TextGenerationResult since the output format is the same
const CONFIG: {
webId: string;
availableProviders: {
text: string[];
visionEnabled: string[];
};
} = {
webId: "6818365e6da47c0013cf7afd",
availableProviders: {
"text": [
"azure-gpt-4o"
],
"visionEnabled": [
"azure-gpt-4o"
]
}
};
const API_KEYS = {};
// ===== TEXT GENERATION MODULE =====
export interface TextProviderImplementation {
generate(prompt: string): Promise<TextGenerationResult>;
generateStream(prompt: string, onChunk: (text: string) => void): Promise<TextGenerationResult>;
}
export interface VisionTextProviderImplementation extends TextProviderImplementation {
generateWithImages(prompt: string, imageUrls: string[]): Promise<TextGenerationResult>;
generateWithImagesStream(prompt: string, imageUrls: string[], onChunk: (text: string) => void): Promise<TextGenerationResult>;
}
/**
* Generate text using a specific provider
* @param prompt - The prompt to generate text from
* @param provider - The provider to use
* @returns A promise that resolves to the generated text
*/
export async function generateText(
prompt: string,
provider: string
): Promise<TextGenerationResult> {
if (!prompt) throw new Error('Prompt is required');
if (!provider) throw new Error('Provider is required');
if (!CONFIG.availableProviders.text.includes(provider)) {
throw new Error(`Provider ${provider} is not available`);
}
try {
return await textProviderImplementations[provider].generate(prompt);
} catch (error) {
if (error instanceof AIServiceError) {
throw error;
} else {
throw new AIServiceError(
`Error generating text: ${error instanceof Error ? error.message : String(error)}`,
500,
provider
);
}
}
}
/**
* Generate text using a specific provider and stream the results
* @param prompt - The prompt to generate text from
* @param onChunk - A callback function that receives a chunk of text, can be used to update a UI or display the text in real-time
* @param provider - The provider to use
* @returns A promise that resolves to the generated text
*/
export async function generateTextStream(
prompt: string,
onChunk: (text: string) => void,
provider: string
): Promise<TextGenerationResult> {
if (!prompt) throw new Error('Prompt is required');
if (!onChunk) throw new Error('onChunk callback is required');
if (!provider) throw new Error('Provider is required');
if (!CONFIG.availableProviders.text.includes(provider)) {
throw new Error(`Provider ${provider} is not available`);
}
try {
return await textProviderImplementations[provider].generateStream(prompt, onChunk);
} catch (error) {
if (error instanceof AIServiceError) {
throw error;
} else {
throw new AIServiceError(
`Error streaming text: ${error instanceof Error ? error.message : String(error)}`,
500,
provider
);
}
}
}
/**
* Get all available text providers
* @returns An array of all available text providers
*/
export function getTextProviders(): string[] {
return CONFIG.availableProviders.text;
}
/**
* Get all available vision-enabled providers
* @returns An array of all available vision-enabled providers
*/
export function getVisionEnabledProviders(): string[] {
return CONFIG.availableProviders.visionEnabled;
}
/**
* Generate text using a specific provider and provide images
* @param prompt - The prompt to generate text from
* @param imageUrls - An array of image URLs
* @param provider - The provider to use
* @returns A promise that resolves to the generated text
*/
export async function generateTextWithImages(
prompt: string,
imageUrls: string[],
provider: string
): Promise<TextGenerationResult> {
if (!prompt) throw new Error('Prompt is required');
if (!imageUrls || !Array.isArray(imageUrls)) throw new Error('Image URLs must be an array');
if (!provider) throw new Error('Provider is required');
if (!CONFIG.availableProviders.visionEnabled.includes(provider)) {
throw new Error(`Provider ${provider} does not support image inputs`);
}
try {
const implementation = textProviderImplementations[provider] as VisionTextProviderImplementation;
return await implementation.generateWithImages(prompt, imageUrls);
} catch (error) {
if (error instanceof AIServiceError) {
throw error;
} else {
throw new AIServiceError(
`Error generating text with images: ${error instanceof Error ? error.message : String(error)}`,
500,
provider
);
}
}
}
/**
* Generate text using a specific provider and provide images and stream the results
* @param prompt - The prompt to generate text from
* @param imageUrls - An array of image URLs
* @param onChunk - A callback function that receives a chunk of text, can be used to update a UI or display the text in real-time
* @param provider - The provider to use
* @returns A promise that resolves to the generated text
*/
export async function generateTextWithImagesStream(
prompt: string,
imageUrls: string[],
onChunk: (text: string) => void,
provider: string
): Promise<TextGenerationResult> {
if (!prompt) throw new Error('Prompt is required');
if (!imageUrls || !Array.isArray(imageUrls)) throw new Error('Image URLs must be an array');
if (!onChunk) throw new Error('onChunk callback is required');
if (!provider) throw new Error('Provider is required');
if (!CONFIG.availableProviders.visionEnabled.includes(provider)) {
throw new Error(`Provider ${provider} does not support image inputs`);
}
try {
const implementation = textProviderImplementations[provider] as VisionTextProviderImplementation;
return await implementation.generateWithImagesStream(prompt, imageUrls, onChunk);
} catch (error) {
if (error instanceof AIServiceError) {
throw error;
} else {
throw new AIServiceError(
`Error streaming text with images: ${error instanceof Error ? error.message : String(error)}`,
500,
provider
);
}
}
}
// Shared implementation for all LiteLLM text providers
/**
* Generate text using a specific provider
* @param prompt - The prompt to generate text from
* @param integrationId - The integration ID to use
* @param model - The model to use
* @returns A promise that resolves to the generated text
*/
async function proxyGenerate(prompt: string, integrationId: string, model: string): Promise<TextGenerationResult> {
const requestBody = {
webId: CONFIG.webId,
integrationId,
prompt,
stream: false,
model
};
try {
const response = await fetch('https://proxy.getcreatr.com/v1/proxy/llm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new AIServiceError(
`Proxy error: ${response.status} ${response.statusText}`,
response.status,
model
);
}
const data = await response.json();
console.log(data);
return {
text: data.data.choices[0].message.content,
usage: {
promptTokens: data.data.usage.prompt_tokens,
completionTokens: data.data.usage.completion_tokens,
totalTokens: data.data.usage.total_tokens
},
finishReason: data.data.choices[0].finish_reason
};
} catch (error) {
if (error instanceof AIServiceError) throw error;
throw new Error(`Failed to call proxy: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Generate text using a specific provider and stream the results
* @param prompt - The prompt to generate text from
* @param onChunk - A callback function that receives a chunk of text, can be used to update a UI or display the text in real-time
* @param integrationId - The integration ID to use
* @param model - The model to use
* @returns A promise that resolves to the generated text
*/
async function proxyGenerateStream(prompt: string, onChunk: (text: string) => void, integrationId: string, model: string): Promise<TextGenerationResult> {
const requestBody = {
webId: CONFIG.webId,
integrationId,
prompt,
stream: true,
model
};
let fullText = '';
let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
let finishReason;
try {
const response = await fetch('https://proxy.getcreatr.com/v1/proxy/llm/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new AIServiceError(
`Proxy streaming error: ${response.status} ${response.statusText}`,
response.status,
model
);
}
if (!response.body) throw new Error('Response body is null or undefined');
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let isStreamActive = true;
while (isStreamActive) {
const { done, value } = await reader.read();
if (done) {
isStreamActive = false;
continue;
}
buffer += decoder.decode(value, { stream: true });
const events = buffer.split('\n\n');
buffer = events.pop() || '';
for (const event of events) {
if (event.startsWith('data: ')) {
const data = event.slice(6).trim();
if (data === '[DONE]') {
isStreamActive = false;
continue;
}
try {
const parsed = JSON.parse(data);
const chunkContent = parsed.choices[0]?.delta?.content || '';
if (chunkContent) {
fullText += chunkContent;
onChunk(chunkContent);
}
if (parsed.choices[0]?.finish_reason) {
finishReason = parsed.choices[0].finish_reason;
}
// Update usage information from the final chunk
if (parsed.usage) {
usage = {
promptTokens: parsed.usage.prompt_tokens,
completionTokens: parsed.usage.completion_tokens,
totalTokens: parsed.usage.total_tokens
};
}
} catch (e) {
console.error('Error parsing stream chunk:', e);
}
}
}
}
return { text: fullText, usage, finishReason };
} catch (error) {
if (error instanceof AIServiceError) throw error;
throw new Error(`Failed to stream from proxy: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Shared implementation for vision-enabled text providers
/**
* Generate text using a specific provider and provide images
* @param prompt - The prompt to generate text from
* @param imageUrls - An array of image URLs
* @param integrationId - The integration ID to use
* @param model - The model to use
* @returns A promise that resolves to the generated text
*/
async function proxyGenerateWithImages(prompt: string, imageUrls: string[], integrationId: string, model: string): Promise<TextGenerationResult> {
// Build content array with text and images for each message
const content: ContentItem[] = [
{ type: 'text', text: prompt }
];
// Add each image URL to the content array
for (const url of imageUrls) {
content.push({
type: 'image_url',
image_url: {
url: url
}
});
}
// Create the request payload exactly matching the expected format
const requestBody = {
model,
webId: CONFIG.webId,
integrationId,
messages: [
{
role: 'user',
content
}
],
stream: false
};
try {
const response = await fetch('https://proxy.getcreatr.com/v1/proxy/llm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new AIServiceError(
`Vision proxy error: ${response.status} ${response.statusText}`,
response.status,
model
);
}
const data = await response.json();
console.log(data);
return {
text: data.data.choices[0].message.content,
usage: {
promptTokens: data.data.usage.prompt_tokens,
completionTokens: data.data.usage.completion_tokens,
totalTokens: data.data.usage.total_tokens
},
finishReason: data.data.choices[0].finish_reason
};
} catch (error) {
if (error instanceof AIServiceError) throw error;
throw new Error(`Failed to call vision proxy: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Generate text using a specific provider and provide images and stream the results
* @param prompt - The prompt to generate text from
* @param imageUrls - An array of image URLs
* @param onChunk - A callback function that receives a chunk of text, can be used to update a UI or display the text in real-time
* @param integrationId - The integration ID to use
* @param model - The model to use
* @returns A promise that resolves to the generated text
*/
async function proxyGenerateWithImagesStream(prompt: string, imageUrls: string[], onChunk: (text: string) => void, integrationId: string, model: string): Promise<TextGenerationResult> {
// Build content array with text and images
const content: ContentItem[] = [
{ type: 'text', text: prompt }
];
// Add each image URL to the content array
for (const url of imageUrls) {
content.push({
type: 'image_url',
image_url: {
url: url
}
});
}
// Create the request payload exactly matching the expected format
const requestBody = {
model,
webId: CONFIG.webId,
integrationId,
messages: [
{
role: 'user',
content
}
],
stream: true
};
let fullText = '';
let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
let finishReason;
try {
const response = await fetch('https://proxy.getcreatr.com/v1/proxy/llm/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new AIServiceError(
`Vision proxy streaming error: ${response.status} ${response.statusText}`,
response.status,
model
);
}
if (!response.body) throw new Error('Response body is null or undefined');
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let isStreamActive = true;
while (isStreamActive) {
const { done, value } = await reader.read();
if (done) {
isStreamActive = false;
continue;
}
buffer += decoder.decode(value, { stream: true });
const events = buffer.split('\n\n');
buffer = events.pop() || '';
for (const event of events) {
if (event.startsWith('data: ')) {
const data = event.slice(6).trim();
if (data === '[DONE]') {
isStreamActive = false;
continue;
}
try {
const parsed = JSON.parse(data);
const chunkContent = parsed.choices[0]?.delta?.content || '';
if (chunkContent) {
fullText += chunkContent;
onChunk(chunkContent);
}
if (parsed.choices[0]?.finish_reason) {
finishReason = parsed.choices[0].finish_reason;
}
// Update usage information from the final chunk
if (parsed.usage) {
usage = {
promptTokens: parsed.usage.prompt_tokens,
completionTokens: parsed.usage.completion_tokens,
totalTokens: parsed.usage.total_tokens
};
}
} catch (e) {
console.error('Error parsing stream chunk:', e);
}
}
}
}
return { text: fullText, usage, finishReason };
} catch (error) {
if (error instanceof AIServiceError) throw error;
throw new Error(`Failed to stream from vision proxy: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Text provider implementations
const textProviderImplementations: Record<string, TextProviderImplementation | VisionTextProviderImplementation> = {};
// Vision-enabled text provider: azure-gpt-4o
textProviderImplementations["azure-gpt-4o"] = {
async generate(prompt: string): Promise<TextGenerationResult> {
return proxyGenerate(prompt, "67e52e6e8e1618ee9ba4117f", "azure-gpt-4o");
},
async generateStream(prompt: string, onChunk: (text: string) => void): Promise<TextGenerationResult> {
return proxyGenerateStream(prompt, onChunk, "67e52e6e8e1618ee9ba4117f", "azure-gpt-4o");
},
async generateWithImages(prompt: string, imageUrls: string[]): Promise<TextGenerationResult> {
return proxyGenerateWithImages(prompt, imageUrls, "67e52e6e8e1618ee9ba4117f", "azure-gpt-4o");
},
async generateWithImagesStream(prompt: string, imageUrls: string[], onChunk: (text: string) => void): Promise<TextGenerationResult> {
return proxyGenerateWithImagesStream(prompt, imageUrls, onChunk, "67e52e6e8e1618ee9ba4117f", "azure-gpt-4o");
}
};