+
+
+ Your AI Gateway is now running
+
+
+
+
+
+
+
+
+ 1. Let's make a test request
+The gateway supports 250+ models across 36 AI providers. Choose your provider and API
+ key below.
+
+
+ 🐍 Python
+ 📦 Node.js
+ 🌀 cURL
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2. Create a routing config
+Gateway configs allow you to route requests to different providers and models. You can load balance, set fallbacks, and configure automatic retries & timeouts. Learn more
+
+
+ Simple Config
+ Load Balancing
+ Fallbacks
+ Retries & Timeouts
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 3. More Features to Explore
+Discover advanced capabilities of the Portkey AI Gateway.
+
+
+
+
+ Agents
+Seamlessly integrate with popular agent frameworks like Autogen, CrewAI, and LangChain.
+ + + +Multi-modal AI
+Handle vision, audio, and image generation requests across multiple providers.
+ + + +Guardrails
+Verify LLM inputs and outputs with 20+ pre-built checks or build your own.
+ + + +Supported Providers
+Access 250+ models from 35+ providers including OpenAI, Anthropic, and Google.
+ +
+
+
+
+
+ Real-time Logs
+
+
+
+
+
+
+
+
+
+ Time | +Method | +Endpoint | +Status | +Duration | +Actions | +
---|---|---|---|---|---|
+ + Listening for logs... + | +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/public/main.js b/src/public/main.js
new file mode 100644
index 000000000..0b5e11077
--- /dev/null
+++ b/src/public/main.js
@@ -0,0 +1,708 @@
+// function bedrockConfigNode(vars) {
+// return `,
+// awsAccessKeyId: "${vars.providerDetails?.awsAccessKeyId || '[Click to edit]'}",
+// awsSecretAccessKey: "${vars.providerDetails?.awsSecretAccessKey || '[Click to edit]'}",
+// awsRegion: "${vars.providerDetails?.awsRegion || '[Click to edit]'}"${vars.providerDetails?.awsSessionToken ? `,
+// awsSessionToken: "${vars.providerDetails.awsSessionToken}"` : ''}`;
+// }
+
+// function bedrockConfigPython(vars) {
+// return `,
+// aws_access_key_id="${vars.providerDetails?.awsAccessKeyId || '[Click to edit]'}",
+// aws_secret_access_key="${vars.providerDetails?.awsSecretAccessKey || '[Click to edit]'}",
+// aws_region="${vars.providerDetails?.awsRegion || '[Click to edit]'}"${vars.providerDetails?.awsSessionToken ? `,
+// aws_session_token="${vars.providerDetails.awsSessionToken}"` : ''}`;
+// }
+
+// function bedrockConfigCurl(vars) {
+// return `\n-H "x-portkey-aws-access-key-id: ${vars.providerDetails?.awsAccessKeyId || '[Click to edit]'}" \\
+// -H "x-portkey-aws-secret-access-key: ${vars.providerDetails?.awsSecretAccessKey || '[Click to edit]'}" \\
+// -H "x-portkey-aws-region: ${vars.providerDetails?.awsRegion || '[Click to edit]'}" \\${vars.providerDetails?.awsSessionToken ? `\n-H "x-portkey-aws-session-token: ${vars.providerDetails.awsSessionToken}" \\` : ''}`;
+// }
+
+// function azureConfigCurl(vars) {
+// return `\n-H "x-portkey-azure-resource-name: ${vars.providerDetails?.azureResourceName || '[Click to edit]'}" \\
+// -H "x-portkey-azure-deployment-id: ${vars.providerDetails?.azureDeploymentId || '[Click to edit]'}" \\
+// -H "x-portkey-azure-api-version: ${vars.providerDetails?.azureApiVersion || '[Click to edit]'}" \\
+// -H "x-portkey-azure-model-name: ${vars.providerDetails?.azureModelName || '[Click to edit]'}" \\`;
+// }
+
+// function azureConfigNode(vars) {
+// return `,
+// azureResourceName: "${vars.providerDetails?.azureResourceName || '[Click to edit]'}",
+// azureDeploymentId: "${vars.providerDetails?.azureDeploymentId || '[Click to edit]'}",
+// azureApiVersion: "${vars.providerDetails?.azureApiVersion || '[Click to edit]'}",
+// azureModelName: "${vars.providerDetails?.azureModelName || '[Click to edit]'}"`;
+// }
+
+// function azureConfigPython(vars) {
+// return `,
+// azure_resource_name="${vars.providerDetails?.azureResourceName || '[Click to edit]'}",
+// azure_deployment_id="${vars.providerDetails?.azureDeploymentId || '[Click to edit]'}",
+// azure_api_version="${vars.providerDetails?.azureApiVersion || '[Click to edit]'}",
+// azure_model_name="${vars.providerDetails?.azureModelName || '[Click to edit]'}"`;
+// }
+
+// Case conversion utilities
+function toCamelCase(str) {
+ return str.replace(/([-_][a-z])/g, group =>
+ group.toUpperCase()
+ .replace('-', '')
+ .replace('_', '')
+ );
+}
+
+function toSnakeCase(str) {
+ return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
+}
+
+function toKebabCase(str) {
+ return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
+}
+
+// Base configuration fields for each provider
+const providerConfigs = {
+ bedrock: {
+ fields: [
+ 'awsAccessKeyId',
+ 'awsSecretAccessKey',
+ 'awsRegion',
+ 'awsSessionToken'
+ ]
+ },
+ azure: {
+ fields: [
+ 'azureResourceName',
+ 'azureDeploymentId',
+ 'azureApiVersion',
+ 'azureModelName'
+ ]
+ }
+};
+
+const modelMap = {
+ "openai": "gpt-4o-mini",
+ "anthropic": "claude-3-5-sonnet-20240620",
+ "groq": "llama3-70b-8192",
+ "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0",
+ "azure-openai": "gpt-4o-mini",
+ "cohere": "command-r-plus",
+ "together-ai": "llama-3.1-8b-instruct",
+ "perplexity-ai": "pplx-7b-online",
+ "mistral-ai": "mistral-small-latest",
+ "others": "gpt-4o-mini"
+}
+
+const docsMap = {
+ "openai": "https://portkey.ai/docs/integrations/llms/openai",
+ "anthropic": "https://portkey.ai/docs/integrations/llms/anthropic",
+ "groq": "https://portkey.ai/docs/integrations/llms/groq",
+ "bedrock": "https://portkey.ai/docs/integrations/llms/aws-bedrock",
+ "azure-openai": "https://portkey.ai/docs/integrations/llms/azure-openai",
+ "cohere": "https://portkey.ai/docs/integrations/llms/cohere",
+ "together-ai": "https://portkey.ai/docs/integrations/llms/together-ai",
+ "perplexity-ai": "https://portkey.ai/docs/integrations/llms/perplexity-ai",
+ "mistral-ai": "https://portkey.ai/docs/integrations/llms/mistral-ai",
+ "others": "https://portkey.ai/docs/integrations/llms"
+}
+
+// Format generators for different styles
+const formatGenerators = {
+ node: {
+ formatKey: fieldName => fieldName,
+ separator: ':',
+ indent: ' ',
+ template: (key, separator, value) =>
+ ` ${key}${separator}"${value}"`,
+ joinWith: ',\n',
+ prefix: ',\n',
+ },
+ python: {
+ formatKey: fieldName => toSnakeCase(fieldName),
+ separator: '=',
+ indent: ' ',
+ template: (key, separator, value) =>
+ ` ${key}${separator}"${value}"`,
+ joinWith: ',\n',
+ prefix: ',\n',
+ },
+ curl: {
+ formatKey: fieldName => `x-portkey-${toKebabCase(fieldName)}`,
+ separator: ':',
+ indent: '',
+ template: (key, separator, value) =>
+ `-H "${key}${separator} ${value}" \\`,
+ joinWith: '\n',
+ prefix: '\n',
+ }
+};
+
+// Helper function to generate highlighted value span
+function generateHighlightedValue(value, id) {
+ const isEmpty = !value;
+ const displayValue = value || '[Click to edit]';
+ return `${displayValue}`;
+}
+
+// Generic config generator function
+function generateConfig(provider, format, vars) {
+ const { fields } = providerConfigs[provider];
+ const formatter = formatGenerators[format];
+ const details = vars.providerDetails || {};
+
+ const configLines = fields
+ .map(fieldName => {
+ // Skip session token if not provided (for Bedrock)
+ if (fieldName === 'awsSessionToken' && !details[fieldName]) return null;
+
+ const key = formatter.formatKey(fieldName);
+ const value = generateHighlightedValue(details[fieldName], fieldName);
+ return formatter.template(key, formatter.separator, value);
+ })
+ .filter(Boolean)
+ .join(formatter.joinWith);
+
+ return formatter.prefix + configLines;
+}
+
+
+function getTestRequestCodeBlock(language, vars) {
+ switch (language) {
+ case 'nodejs':
+ return `
+import Portkey from 'portkey-ai'
+
+const portkey = new Portkey({
+ provider: "${vars.provider || '[Click to edit]'}"${vars.provider != 'bedrock' ? `,
+ Authorization: "${vars.providerDetails?.apiKey || '[Click to edit]'}"`: ''}${vars.provider === 'azure-openai' ? `${generateConfig('azure', 'node', vars)}` : ''}${vars.provider === 'bedrock' ? `${generateConfig('bedrock', 'node', vars)}` : ''}
+})
+
+// Example: Send a chat completion request
+const response = await portkey.chat.completions.create({
+ messages: [{ role: 'user', content: 'Hello, how are you?' }],
+ model: "${modelMap[vars.provider] || ''}"${vars.provider=="anthropic"?`,
+ max_tokens: 40`:''}
+})
+console.log(response.choices[0].message.content)`.trim();
+
+ case 'python':
+ return `
+from portkey_ai import Portkey
+
+client = Portkey(
+ provider="${vars.provider || '[Click to edit]'}"${vars.provider != 'bedrock' ? `,
+ Authorization="${vars.providerDetails?.apiKey || '[Click to edit]'}"`: ''}${vars.provider === 'azure-openai' ? `${generateConfig('azure', 'python', vars)}` : ''}${vars.provider === 'bedrock' ? `${generateConfig('bedrock', 'python', vars)}` : ''}
+)
+
+# Example: Send a chat completion request
+response = client.chat.completions.create(
+ messages=[{"role": "user", "content": "Hello, how are you?"}],
+ model="${modelMap[vars.provider] || ''}"
+)
+print(response.choices[0].message.content)`.trim();
+
+ case 'curl':
+ return `curl -X POST \\
+https://api.portkey.ai/v1/chat/completions \\
+-H "Content-Type: application/json" \\
+-H "x-portkey-provider: ${vars.provider || '[Click to edit]'}" \\${vars.provider != 'bedrock' ? `
+-H "Authorization: ${vars.providerDetails?.apiKey || '[Click to edit]'}" \\`: '' }${vars.provider === 'azure-openai' ? `${generateConfig('azure', 'curl', vars)}` : ''}${vars.provider === 'bedrock' ? `${generateConfig('bedrock', 'curl', vars)}` : ''}
+-d '{
+ "messages": [
+ { "role": "user", "content": "Hello, how are you?" },
+ ],
+ "model": ""${modelMap[vars.provider] || ''}""
+}'`.trim();
+ }
+}
+
+
+function getRoutingConfigCodeBlock(language, type) {
+ return configs[language][type];
+}
+
+// Needed for highlight.js
+const lngMap = {"nodejs": "js", "python": "py", "curl": "sh"}
+
+// Initialize Lucide icons
+lucide.createIcons();
+
+// Variables
+let provider = '';
+let apiKey = '';
+let providerDetails = {};
+let logCounter = 0;
+
+// DOM Elements
+const providerValue = document.getElementById('providerValue');
+const apiKeyValue = document.getElementById('apiKeyValue');
+const copyBtn = document.getElementById('copyBtn');
+const testRequestBtn = document.getElementById('testRequestBtn');
+const logsContent = document.getElementById('logsContent');
+const providerDialog = document.getElementById('providerDialog');
+const apiKeyDialog = document.getElementById('apiKeyDialog');
+const providerSelect = document.getElementById('providerSelect');
+const apiKeyInput = document.getElementById('apiKeyInput');
+const saveApiKeyBtn = document.getElementById('saveApiKeyBtn');
+const saveApiDetailsBtn = document.getElementById('saveApiDetailsBtn');
+const languageSelect = document.getElementById('languageSelect');
+const copyConfigBtn = document.getElementById('copyConfigBtn');
+
+const camelToSnakeCase = str => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
+const camelToKebabCase = str => str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
+
+// Dummy function for test request
+function dummyTestRequest() {
+ // Make an API request to the Portkey API
+ // Use the provider and providerDetails to make the request
+ const myHeaders = new Headers();
+ Object.keys(providerDetails).forEach(key => {
+ if (key === 'apiKey') {
+ myHeaders.append("Authorization", providerDetails[key]);
+ } else {
+ myHeaders.append("x-portkey-" + camelToKebabCase(key), providerDetails[key]);
+ }
+ })
+ myHeaders.append("Content-Type", "application/json");
+ myHeaders.append("x-portkey-provider", provider);
+
+ const raw = JSON.stringify({
+ "messages": [{"role": "user","content": "How are you?"}],
+ "model": modelMap[provider],
+ "max_tokens": 40
+ });
+
+ const requestOptions = {method: "POST", headers: myHeaders, body: raw};
+
+ // Add loading class to testRequestBtn
+ testRequestBtn.classList.add('loading');
+
+ fetch("/v1/chat/completions", requestOptions)
+ .then((response) => {
+ if (!response.ok) {
+ return response.json().then(error => {
+ const responseDiv = document.getElementById('testRequestResponse');
+ responseDiv.innerHTML = `[${response.status} ${response.statusText}]: ${error.message || error.error.message}`;
+ responseDiv.style.display = 'block';
+ throw new Error(error);
+ });
+ }
+ return response.json();
+ })
+ .then((result) => {
+ const responseDiv = document.getElementById('testRequestResponse');
+ responseDiv.innerHTML = `${result.choices[0].message.content}`;
+ responseDiv.style.display = 'block';
+ responseDiv.classList.remove('error');
+ })
+ .catch((error) => {
+ console.error('Error:', error);
+ })
+ .finally(() => {
+ // Remove loading class from testRequestBtn
+ testRequestBtn.classList.remove('loading');
+ });
+}
+
+// Functions
+
+function switchTab(tabsContainer, tabName, updateRoutingConfigFlag = true) {
+ const tabs = tabsContainer.querySelectorAll('.tab');
+ const tabContents = tabsContainer.closest('.card').querySelectorAll('.tab-content');
+
+ tabs.forEach(tab => tab.classList.remove('active'));
+ tabContents.forEach(content => content.classList.remove('active'));
+
+ tabsContainer.querySelector(`.tab[data-tab="${tabName}"]`).classList.add('active');
+ tabsContainer.closest('.card').querySelector(`#${tabName}Content`).classList.add('active');
+
+ if (tabsContainer.classList.contains('test-request-tabs')) {
+ updateAllCommands();
+ // Update the language select with the active tab
+ languageSelect.value = tabName;
+ updateRoutingConfigFlag ? updateRoutingConfig() : null;
+ } else if (tabsContainer.classList.contains('routing-config-tabs')) {
+ updateRoutingConfig();
+ }
+}
+
+function updateAllCommands() {
+ ["nodejs", "python", "curl"].forEach(language => {
+ const command = document.getElementById(`${language}Command`);
+ const code = getTestRequestCodeBlock(language, {provider, providerDetails});
+ command.innerHTML = code;
+ if (provider) {
+ const docsLink = document.querySelector('.docs-link');
+ docsLink.innerHTML = `View detailed docs for ${provider == "others" ? "all providers" : provider} `;
+ docsLink.style.display = 'inline-block';
+ }
+ });
+ addClickListeners();
+}
+
+function highlightElement(element) {
+ element.classList.add('animate-highlight');
+ setTimeout(() => element.classList.remove('animate-highlight'), 1000);
+}
+
+function showProviderDialog() {
+ providerDialog.style.display = 'flex';
+}
+
+function getProviderFields(provider) {
+ switch(provider) {
+ case 'openai':
+ case 'anthropic':
+ case 'groq':
+ return [{ id: 'apiKey', placeholder: 'Enter your API key' }];
+ case 'azure-openai':
+ return [
+ { id: 'apiKey', placeholder: 'Enter your API key' },
+ { id: 'azureResourceName', placeholder: 'Azure Resource Name' },
+ { id: 'azureDeploymentId', placeholder: 'Azure Deployment ID' },
+ { id: 'azureApiVersion', placeholder: 'Azure API Version' },
+ { id: 'azureModelName', placeholder: 'Azure Model Name' }
+ ];
+ case 'bedrock':
+ return [
+ { id: 'awsAccessKeyId', placeholder: 'AWS Access Key ID' },
+ { id: 'awsSecretAccessKey', placeholder: 'AWS Secret Access Key' },
+ { id: 'awsRegion', placeholder: 'AWS Region' },
+ { id: 'awsSessionToken', placeholder: 'AWS Session Token (optional)' }
+ ];
+ default:
+ return [{ id: 'apiKey', placeholder: 'Enter your API key' }];
+ }
+}
+
+function showApiKeyDialog() {
+ // apiKeyDialog.style.display = 'flex';
+ const form = document.getElementById('apiDetailsForm');
+ form.innerHTML = ''; // Clear existing fields
+
+ const fields = getProviderFields(provider);
+ fields.forEach(field => {
+ const label = document.createElement('label');
+ label.textContent = field.placeholder;
+ label.for = field.id;
+ form.appendChild(label);
+ const input = document.createElement('input');
+ // input.type = 'password';
+ input.id = field.id;
+ input.className = 'input';
+ // input.placeholder = field.placeholder;
+ input.value = providerDetails[field.id] || "";
+ form.appendChild(input);
+ });
+
+ apiKeyDialog.style.display = 'flex';
+}
+
+function updateRoutingConfig() {
+ const language = languageSelect.value;
+ const activeTab = document.querySelector('.routing-config-tabs .tab.active').dataset.tab;
+ const codeElement = document.getElementById(`${activeTab}Code`);
+
+ // Also change the tabs for test request
+ switchTab(document.querySelector('.test-request-tabs'), language, false);
+
+ const code = getRoutingConfigCodeBlock(language, activeTab);
+ codeElement.innerHTML = hljs.highlight(code, {language: lngMap[language]}).value;
+}
+
+function addClickListeners() {
+ const providerValueSpans = document.querySelectorAll('.highlighted-value:not(#providerValue)');
+ const providerValues = document.querySelectorAll('[id^="providerValue"]');
+ // const apiKeyValues = document.querySelectorAll('[id^="apiKeyValue"]');
+
+ providerValues.forEach(el => el.addEventListener('click', showProviderDialog));
+ // apiKeyValues.forEach(el => el.addEventListener('click', showApiKeyDialog));
+ providerValueSpans.forEach(el => el.addEventListener('click', showApiKeyDialog));
+}
+
+
+// Event Listeners
+testRequestBtn.addEventListener('click', dummyTestRequest);
+
+document.querySelectorAll('.tabs').forEach(tabsContainer => {
+ tabsContainer.querySelectorAll('.tab').forEach(tab => {
+ tab.addEventListener('click', () => switchTab(tabsContainer, tab.dataset.tab));
+ });
+});
+
+copyBtn.addEventListener('click', () => {
+ const activeContent = document.querySelector('.curl-command .tab-content.active code');
+ navigator.clipboard.writeText(activeContent.innerText);
+ copyBtn.innerHTML = '';
+ lucide.createIcons();
+ setTimeout(() => {
+ copyBtn.innerHTML = '';
+ lucide.createIcons();
+ }, 2000);
+ // addLog('Code example copied to clipboard');
+});
+
+copyConfigBtn.addEventListener('click', () => {
+ const activeContent = document.querySelector('.routing-config .tab-content.active code');
+ navigator.clipboard.writeText(activeContent.textContent);
+ copyConfigBtn.innerHTML = '';
+ lucide.createIcons();
+ setTimeout(() => {
+ copyConfigBtn.innerHTML = '';
+ lucide.createIcons();
+ }, 2000);
+ // addLog('Routing config copied to clipboard');
+});
+
+// Modify existing event listeners
+providerSelect.addEventListener('change', (e) => {
+ provider = e.target.value;
+ updateAllCommands();
+ providerDialog.style.display = 'none';
+ highlightElement(document.getElementById('providerValue'));
+ // Find if there are any provider details in localStorage for this provider
+ let localDetails = localStorage.getItem(`providerDetails-${provider}`);
+ if(localDetails) {
+ console.log('Provider details found in localStorage', localDetails);
+ providerDetails = JSON.parse(localDetails);
+ updateAllCommands();
+ highlightElement(document.getElementById('apiKeyValue'));
+ }
+ // addLog(`Provider set to ${provider}`);
+});
+
+saveApiDetailsBtn.addEventListener('click', () => {
+ const fields = getProviderFields(provider);
+ providerDetails = {};
+ fields.forEach(field => {
+ const input = document.getElementById(field.id);
+ providerDetails[field.id] = input.value;
+ });
+ // Save all provider details in localStorage for this provider
+ localStorage.setItem(`providerDetails-${provider}`, JSON.stringify(providerDetails));
+ updateAllCommands();
+ apiKeyDialog.style.display = 'none';
+ highlightElement(document.getElementById('apiKeyValue'));
+});
+
+languageSelect.addEventListener('change', updateRoutingConfig);
+
+// Initialize
+updateAllCommands();
+updateRoutingConfig();
+
+// Close dialogs when clicking outside
+window.addEventListener('click', (e) => {
+ if (e.target.classList.contains('dialog-overlay')) {
+ e.target.style.display = 'none';
+ }
+});
+
+// Close dialogs when hitting escape
+window.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ providerDialog.style.display = 'none';
+ apiKeyDialog.style.display = 'none';
+ logDetailsModal.style.display = 'none';
+ }
+});
+
+// Tab functionality
+const tabButtons = document.querySelectorAll('.tab-button');
+const tabContents = document.querySelectorAll('.main-tab-content');
+
+function mainTabFocus(tabName) {
+ if(tabName === 'logs') {
+ resetLogCounter();
+ }
+ tabButtons.forEach(btn => btn.classList.remove('active'));
+ tabContents.forEach(content => content.classList.remove('active'));
+
+ document.getElementById(`${tabName}-tab-button`).classList.add('active');
+ document.getElementById(`${tabName}-tab`).classList.add('active');
+}
+
+tabButtons.forEach(button => {
+ button.addEventListener('click', (e) => {
+ e.preventDefault();
+ let tabName = button.getAttribute('data-tab');
+ const href = tabName === 'logs' ? '/public/logs' : '/public/';
+ history.pushState(null, '', href);
+ mainTabFocus(tabName);
+ });
+});
+
+function managePage() {
+ if(window.location.pathname === '/public/logs') {
+ mainTabFocus('logs');
+ } else {
+ mainTabFocus('main');
+ }
+}
+
+window.addEventListener('popstate', () => {
+ managePage()
+});
+
+managePage()
+
+// Logs functionality
+const logsTableBody = document.getElementById('logsTableBody');
+const logDetailsModal = document.getElementById('logDetailsModal');
+const logDetailsContent = document.getElementById('logDetailsContent');
+const closeModal = document.querySelector('.close');
+const clearLogsBtn = document.querySelector('.btn-clear-logs');
+
+// SSE for the logs
+let logSource;
+
+function setupLogSource() {
+ logSource = new EventSource('/log/stream');
+
+ logSource.addEventListener('connected', (event) => {
+ console.log('Connected to log stream', event.data);
+ });
+
+ logSource.addEventListener('log', (event) => {
+ const entry = JSON.parse(event.data);
+ console.log('Received log entry', entry);
+ addLogEntry(entry.time, entry.method, entry.endpoint, entry.status, entry.duration, entry.requestOptions);
+ });
+
+ // Handle heartbeat to keep connection alive
+ logSource.addEventListener('heartbeat', (event) => {
+ console.log('Received heartbeat');
+ });
+
+ logSource.onerror = (error) => {
+ console.error('SSE error (logs):', error);
+ reconnectLogSource();
+ };
+}
+
+function cleanupLogSource() {
+ if (logSource) {
+ console.log('Closing log stream connection');
+ logSource.close();
+ logSource = null;
+ }
+}
+
+function reconnectLogSource() {
+ if (logSource) {
+ logSource.close();
+ }
+ console.log('Attempting to reconnect to log stream...');
+ setTimeout(() => {
+ setupLogSource();
+ }, 5000); // Wait 5 seconds before attempting to reconnect
+}
+
+setupLogSource();
+
+function addLogEntry(time, method, endpoint, status, duration, requestOptions) {
+ const tr = document.createElement('tr');
+ tr.classList.add('new-row');
+ tr.innerHTML = `
+ Request Details
+Time: ${time}
+Method: ${method}
+Endpoint: ${endpoint}
+Status: ${status}
+Duration: ${duration}ms
+Request:
${JSON.stringify(requestOptions[0].requestParams, null, 2)}+
Response:
${JSON.stringify(requestOptions[0].response, null, 2)}+ `; + logDetailsModal.style.display = 'block'; +} + +function incrementLogCounter() { + if(window.location.pathname != '/public/logs') { + logCounter++; + const badge = document.querySelector('header .badge'); + badge.textContent = logCounter; + badge.style.display = 'inline-block'; + } +} + +function resetLogCounter() { + logCounter = 0; + const badge = document.querySelector('header .badge'); + badge.textContent = logCounter; + badge.style.display = 'none'; +} + +closeModal.addEventListener('click', () => { + logDetailsModal.style.display = 'none'; +}); + +window.addEventListener('click', (event) => { + if (event.target === logDetailsModal) { + logDetailsModal.style.display = 'none'; + } +}); + +// Update event listeners for page unload +window.addEventListener('beforeunload', cleanupLogSource); +window.addEventListener('unload', cleanupLogSource); + + +window.onload = function() { + // Run the confetti function only once by storing the state in localStorage + if(!localStorage.getItem('confettiRun')) { + setTimeout(() => { + confetti(); + localStorage.setItem('confettiRun', 'true'); + }, 1000); + } + // confetti({ + // particleCount: 100, + // spread: 70, + // origin: { y: 0.6 } + // }); +}; diff --git a/src/public/snippets.js b/src/public/snippets.js new file mode 100644 index 000000000..ee0316e69 --- /dev/null +++ b/src/public/snippets.js @@ -0,0 +1,230 @@ +const configs = {"nodejs": {}, "python": {}, "curl": {}} + +// Node.js - Simple +configs["nodejs"]["simple"] = ` +// 1. Create config with provider and API key +const config = { + "provider": 'openai', + "api_key": 'Your OpenAI API key', +}; + +// 2. Add this config to the client +const client = new Portkey({config}); + +// 3. Use the client in completion requests +await client.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello, world!' }], +});` + +// Node.js - Load Balancing +configs["nodejs"]["loadBalancing"] = ` +// 1. Create the load-balanced config +const lbConfig = { + "strategy": { "mode": "loadbalance" }, + "targets": [{ + "provider": 'openai', + "api_key": 'Your OpenAI API key', + "weight": 0.7 + },{ + "provider": 'anthropic', + "api_key": 'Your Anthropic API key', + "weight": 0.3, + "override_params": { + "model": 'claude-3-opus-20240229' // Any params you want to override + }, + }], +}; + +// 2. Use the config in completion requests +await client.chat.completions.create({ + model: 'gpt-4o', // The model will be replaced with the one specified in the config + messages: [{ role: 'user', content: 'Hello, world!' }], +}, {config: lbConfig});` + +// Node.js - Fallbacks +configs["nodejs"]["fallbacks"] = ` +// 1. Create the fallback config +const fallbackConfig = { + "strategy": { "mode": "fallback" }, + "targets": [{ // The primary target + "provider": 'openai', + "api_key": 'Your OpenAI API key', + },{ // The fallback target + "provider": 'anthropic', + "api_key": 'Your Anthropic API key', + }], +}; + +// 2. Use the config in completion requests +await client.chat.completions.create({ + model: 'gpt-4o', // The model will be replaced with the one specified in the config + messages: [{ role: 'user', content: 'Hello, world!' }], +}, {config: fallbackConfig});` + +// Node.js - Retries & Timeouts +configs["nodejs"]["autoRetries"] = ` +// 1. Create the retry and timeout config +const retryTimeoutConfig = { + "retry": { + "attempts": 3, + "on_status_codes": [429, 502, 503, 504] // Optional + }, + "request_timeout": 10000, + "provider": 'openai', + "api_key": 'Your OpenAI API key' +}; + +// 2. Use the config in completion requests +await client.chat.completions.create({ + model: 'gpt-4o', // The model will be replaced with the one specified in the config + messages: [{ role: 'user', content: 'Hello, world!' }], +}, {config: retryTimeoutConfig});` + +// Python - Simple +configs["python"]["simple"] = ` +# 1. Create config with provider and API key +config = { + "provider": 'openai', + "api_key": 'Your OpenAI API key', +} + +# 2. Add this config to the client +client = Portkey(config=config) + +# 3. Use the client in completion requests +client.chat.completions.create( + model = 'gpt-4o', + messages = [{ role: 'user', content: 'Hello, world!' }], +)` + +// Python - Load Balancing +configs["python"]["loadBalancing"] = ` +# 1. Create the load-balanced config +lb_config = { + "strategy": { "mode": "loadbalance" }, + "targets": [{ + "provider": 'openai', + "api_key": 'Your OpenAI API key', + "weight": 0.7 + },{ + "provider": 'anthropic', + "api_key": 'Your Anthropic API key', + "weight": 0.3, + "override_params": { + "model": 'claude-3-opus-20240229' # Any params you want to override + }, + }], +} + +# 2. Use the config in completion requests +client.with_options(config=lb_config).chat.completions.create( + model = 'gpt-4o', + messages = [{ role: 'user', content: 'Hello, world!' }], +)` + +// Python - Fallbacks +configs["python"]["fallbacks"] = ` +# 1. Create the fallback config +fallback_config = { + "strategy": { "mode": "fallback" }, + "targets": [{ # The primary target + "provider": 'openai', + "api_key": 'Your OpenAI API key', + },{ # The fallback target + "provider": 'anthropic', + "api_key": 'Your Anthropic API key', + "override_params": { + "model": 'claude-3-opus-20240229' # Any params you want to override + }, + }], +} + +# 2. Use the config in completion requests +client.with_options(config=fallback_config).chat.completions.create( + model = 'gpt-4o', + messages = [{ role: 'user', content: 'Hello, world!' }], +)` + +// Python - Retries & Timeouts +configs["python"]["autoRetries"] = ` +# 1. Create the retry and timeout config +retry_timeout_config = { + "retry": { + "attempts": 3, + "on_status_codes": [429, 502, 503, 504] # Optional + }, + "request_timeout": 10000, + "provider": 'openai', + "api_key": 'Your OpenAI API key' +} + +# 2. Use the config in completion requests +client.with_options(config=retry_timeout_config).chat.completions.create( + model = 'gpt-4o', + messages = [{ role: 'user', content: 'Hello, world!' }], +)` + +// Curl - Simple +configs["curl"]["simple"] = ` +# Store the config in a variable +simple_config='{"provider":"openai","api_key":"Your OpenAI API Key"}' + +# Use the config in completion requests +curl http://localhost:8787/v1/chat/completions \ +\n-H "Content-Type: application/json" \ +\n-H "x-portkey-config: $simple_config" \ +\n-d '{ + "model": "gpt-4o", + "messages": [ + { "role": "user", "content": "Hello!" } + ] +}'` + +// Curl - Load Balancing +configs["curl"]["loadBalancing"] = ` +# Store the config in a variable +lb_config='{"strategy":{"mode":"loadbalance"},"targets":[{"provider":"openai","api_key":"Your OpenAI API key","weight": 0.7 },{"provider":"anthropic","api_key":"Your Anthropic API key","weight": 0.3,"override_params":{"model":"claude-3-opus-20240229"}}]}' + +# Use the config in completion requests +curl http://localhost:8787/v1/chat/completions \ +\n-H "Content-Type: application/json" \ +\n-H "x-portkey-config: $lb_config" \ +\n-d '{ + "model": "gpt-4o", + "messages": [ + { "role": "user", "content": "Hello!" } + ] +}'` + +// Curl - Fallbacks +configs["curl"]["fallbacks"] = ` +# Store the config in a variable +fb_config='{"strategy":{"mode":"fallback"},"targets":[{"provider":"openai","api_key":"Your OpenAI API key"},{"provider":"anthropic","api_key":"Your Anthropic API key","override_params":{"model":"claude-3-opus-20240229"}}]}' + +# Use the config in completion requests +curl http://localhost:8787/v1/chat/completions \ +\n-H "Content-Type: application/json" \ +\n-H "x-portkey-config: $fb_config" \ +\n-d '{ + "model": "gpt-4o", + "messages": [ + { "role": "user", "content": "Hello!" } + ] +}'` + +// Curl - Retries & Timeouts +configs["curl"]["autoRetries"] = ` +# Store the config in a variable +rt_config='{"retry":{"attempts": 3,"on_status_codes": [429, 502, 503, 504]},"request_timeout": 10000, "provider": "openai", "api_key": "Your OpenAI API key"}' + +# Use the config in completion requests +curl http://localhost:8787/v1/chat/completions \ +\n-H "Content-Type: application/json" \ +\n-H "x-portkey-config: $rt_config" \ +\n-d '{ + "model": "gpt-4o", + "messages": [ + { "role": "user", "content": "Hello!" } + ] +}'` \ No newline at end of file diff --git a/src/public/styles/buttons.css b/src/public/styles/buttons.css new file mode 100644 index 000000000..954aa297e --- /dev/null +++ b/src/public/styles/buttons.css @@ -0,0 +1,63 @@ +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; + background-color: rgb(24, 24, 27); + color: white; + border: 0px; + position: relative; + overflow: hidden; +} + +.btn:hover { + background-color: rgba(24, 24, 27,0.9) +} + +.btn-outline { + border: 1px solid #b8bcc2; + background-color: white; + color: rgb(24, 24, 27); +} + +.btn-outline:hover { + background-color: #f3f4f6; +} + +/* Loading state */ +.btn.loading { + cursor: not-allowed; + opacity: 0.7; +} + +.btn.loading::after { + content: ''; + position: absolute; + width: 1rem; + height: 1rem; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: white; + animation: spin 0.8s linear infinite; +} + +.btn.loading .btn-text { + visibility: hidden; +} + +.btn-outline.loading::after { + border-color: rgba(24, 24, 27, 0.3); + border-top-color: rgb(24, 24, 27); +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src/public/styles/header.css b/src/public/styles/header.css new file mode 100644 index 000000000..9a49b7b44 --- /dev/null +++ b/src/public/styles/header.css @@ -0,0 +1,103 @@ +/* Header styles */ +header { + background-color: white; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + padding: 0.75rem 0; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; +} + +.container { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 1rem; +} + +.logo { + display: flex; + align-items: center; +} + +.logo img { + margin-right: 0.5rem; + max-height: 2rem; +} + +.logo span { + font-size: 0.875rem; + font-weight: normal; + display: flex; + align-items: center; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #22c55e; + margin-left: 8px; + animation: blink 1s infinite; +} + +@keyframes blink { + 0% { opacity: 0; } + 50% { opacity: 1; } + 100% { opacity: 0; } +} + +.header-links { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.header-links a { + color: #2563eb; + text-decoration: none; + font-size: 0.875rem; +} + +.header-links a:hover { + color: #1d4ed8; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .container { + flex-direction: column; + align-items: flex-start; + } + + .logo { + margin-bottom: 0.5rem; + } + + .tabs-container { + margin-bottom: 0.5rem; + } + + .header-links { + width: 100%; + justify-content: space-between; + } +} + +header .badge { + background-color: rgb(239, 68, 68); + color: white; + padding: 0.25rem 0.25rem; + border-radius: 100px; + font-size: 0.65rem; + font-weight: normal; + margin-left: 5px; + min-width: 13px; + /* display: inline-block; */ + text-align: center; + display: none; +} \ No newline at end of file diff --git a/src/public/styles/interative-code.css b/src/public/styles/interative-code.css new file mode 100644 index 000000000..9c0b30b13 --- /dev/null +++ b/src/public/styles/interative-code.css @@ -0,0 +1,178 @@ +pre { + background-color: #f3f4f6; + padding: 0.75rem; + border-radius: 0.375rem; + overflow-x: auto; + font-size: 0.875rem; + position: relative; +} + +.copy-btn { + position: absolute; + top: 0.5rem; + right: 0.5rem; + padding: 0.25rem; + background-color: white; + border: 1px solid #d1d5db; + border-radius: 0.25rem; + cursor: pointer; + z-index: 10; + height: 28px; +} + +.copy-btn svg { + width: 20px; + height: 18px; + color: #393d45; +} + +/* Highlighted values */ +.highlighted-value { + display: inline-block; + position: relative; + cursor: pointer; + transition: transform 0.2s; + padding: 0 0.25rem; + margin: 2px 0; +} + +.highlighted-value.filled { + font-weight: bold; +} + +.highlighted-value:hover { + transform: scale(1.05); +} + +.highlighted-value::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 0.25rem; + transition: all 0.2s; +} + +.highlighted-value.empty::before { + background-color: rgba(252, 165, 165, 0.3); + border: 1px solid rgba(248, 113, 113, 0.5); +} + +.highlighted-value.filled::before { + background-color: rgba(134, 239, 172, 0.2); + border: 1px solid rgba(74, 222, 128, 0.5); +} + +.highlighted-value:hover::before { + opacity: 0.4; +} + +.highlighted-value span { + position: relative; + z-index: 10; +} + +.highlighted-value.empty span { + color: #dc2626; +} + +.highlighted-value.filled span { + color: #16a34a; +} + +@keyframes highlight { + 0% { + background-color: rgba(253, 224, 71, 0.2); + transform: scale(1); + } + 20% { + background-color: rgba(253, 224, 71, 1); + transform: scale(1.05); + } + 100% { + background-color: rgba(253, 224, 71, 0.2); + transform: scale(1); + } +} + +/* Dialog styles */ +.dialog-overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 50; +} + +.dialog { + background-color: white; + border-radius: 0.5rem; + padding: 1.5rem; + width: 90%; + max-width: 500px; +} + +.dialog h3 { + font-size: 1.25rem; + font-weight: bold; + margin-bottom: 0; + margin-top: 0; +} + +.dialog p { + font-size: 0.875rem; + color: #6b7280; + margin-bottom: 1rem; + margin-top: 0; +} + +.select-wrapper { + position: relative; +} + +.select { + width: 100%; + padding: 0.5rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + appearance: none; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + font-size: 0.75rem; +} + +.input { + width: 90%; + padding: 0.5rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + margin-bottom: 0.5rem; +} + +.dialog label { + font-size: 0.75rem; + font-weight: bold; + display: inline-block; + padding: 0.25rem; +} + +.dialog .btn { + margin-top: 0.5rem; +} + +.animate-highlight { + animation: highlight 1s ease-out; +} + +.language-select-wrapper { + width: 100px; + display: inline-block; + position: absolute; + z-index: 2; + right: 45px; + top: 0.5rem; + font-size: 12px; +} \ No newline at end of file diff --git a/src/public/styles/logs.css b/src/public/styles/logs.css new file mode 100644 index 000000000..0f7420438 --- /dev/null +++ b/src/public/styles/logs.css @@ -0,0 +1,150 @@ +/* Logs styles */ +.card.logs-card { + margin-top: 1rem; + max-width: 800px; +} + +.logs-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.logs-table-container { + background-color: white; + border-radius: 8px; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + overflow: hidden; + width: 100%; + max-width: 800px; +} + +.logs-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; +} + +.logs-table th, +.logs-table td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid #e5e7eb; + cursor: default; +} + +.logs-table th { + background-color: #f3f4f6; + font-weight: 600; + color: #374151; + text-transform: uppercase; + font-size: 12px; + letter-spacing: 0.05em; +} + +.logs-table tr:hover { + background-color: #f3f4f6; +} + +/* .logs-table td:last-child { + text-align: right; +} */ + +.loading-row { + background-color: #f3f4f6; + color: #6b7280; + font-style: italic; +} + +.loading-row td { + padding: 0.25rem 0.75rem; +} + +.loading-animation { + display: inline-block; + width: 12px; + height: 12px; + border: 2px solid #6b7280; + border-radius: 50%; + border-top-color: transparent; + animation: spin 1s linear infinite; + margin-right: 8px; + vertical-align: middle; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.new-row { + animation: fadeInSlideDown 0.2s ease-out; +} +@keyframes fadeInSlideDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.log-time { + font-family: monospace; + font-size: 0.875rem; +} + +.log-method span { + padding: 0.3rem; + background-color: rgba(0, 0, 0, 0.1); + border-radius: 4px; + font-weight: 600; +} + +.log-status span.success { + padding: 0.3rem; + background-color: green; + border-radius: 4px; + color: white; + font-weight: 700; +} + +.log-status span.error { + padding: 0.3rem; + background-color: red; + border-radius: 4px; + color: white; + font-weight: 700; +} + + + +.btn-view-details { + padding: 0.25rem 0.5rem; + background-color: #3b82f6; + color: white; + border: none; + border-radius: 0.25rem; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.btn-view-details:hover { + background-color: #2563eb; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .logs-header { + flex-direction: column; + align-items: stretch; + } + + .logs-search { + width: 100%; + margin-bottom: 1rem; + } +} \ No newline at end of file diff --git a/src/public/styles/modal.css b/src/public/styles/modal.css new file mode 100644 index 000000000..439a4f597 --- /dev/null +++ b/src/public/styles/modal.css @@ -0,0 +1,35 @@ +/* Modal styles */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); +} + +.modal-content { + background-color: white; + margin: 0 0 0 auto; + padding: 2rem; + border-radius: 0rem; + width: 80%; + max-width: 500px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + height: 100vh; + overflow-y: auto; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; +} + +.close:hover { + color: #000; +} \ No newline at end of file diff --git a/src/public/styles/style.css b/src/public/styles/style.css new file mode 100644 index 000000000..cf78ea03f --- /dev/null +++ b/src/public/styles/style.css @@ -0,0 +1,230 @@ +/* Base styles */ +body { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + font-size: 14px; + margin: 0; + padding: 0; + min-height: 100vh; + background-color: #f3f4f6; + color: #213547; + margin-top: 4rem; +} + +a { + color: rgb(24, 24, 27); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.relative { + position: relative; +} + +/* Main content styles */ +.main-content { + max-width: 1200px; + margin: 1rem auto; + padding: 0 1rem; +} + +/* Main content styles */ +.main-content { + max-width: 1200px; + margin: 0 auto; + padding: 1rem; + transition: margin-bottom 0.3s ease; +} + +.left-column { + width: 65%; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.right-column { + width: 35%; + display: flex; + flex-direction: column; +} + +.card { + background-color: white; + border-radius: 0.75rem; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + padding: 1.5rem; + max-width: 600px; + margin: 0rem auto 2rem auto; +} + +.left-column .card { + width: 100%; + max-width: 500px; + margin: 0 auto; +} + +/* Responsive adjustments */ +@media (max-width: 1024px) { + .main-content { + flex-direction: column; + } + + .left-column, + .right-column { + width: 100%; + } +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +h2 { + font-size: 1.125rem; + font-weight: bold; + margin: 0; +} + +.card-subtitle { + font-size: 0.875rem; + color: #6b7280; + margin-bottom: 1rem; +} + +/* Features to Explore Card Styles */ +.features-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + margin-top: 1rem; +} + +.feature-item { + background-color: #f9fafb; + border-radius: 0.5rem; + padding: 1rem; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.feature-item:hover { + box-shadow: 0px 0px 3px 1px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: all 0.2s; + text-decoration: none; +} + +.feature-item .icon { + width: 2rem; + height: 2rem; + color: #3b82f6; + margin-bottom: 0.5rem; +} + +.feature-item h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.feature-item p { + font-size: 0.875rem; + color: #6b7280; +} + +/* Next Steps Card Styles */ +.card.next-steps { + margin-top: 1rem; + background-color: transparent; + /* border-top: 1px solid #ccc; */ + padding-top: 2rem; + box-shadow: none; + border-radius: 0; + width: 90%; + max-width: 700px; +} + +.next-steps-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-top: 1rem; +} + +.next-step-item { + border: 1px solid #babcc0; + border-radius: 0.5rem; + padding: 1rem; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + box-shadow: 0px 5px 3px 2px rgba(0, 0, 0, 0.1); +} + +.next-step-item .icon { + width: 2rem; + height: 2rem; + color: #3b82f6; + margin-bottom: 0.5rem; +} + +.next-step-item h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.next-step-item p { + font-size: 0.875rem; + color: #6b7280; + margin-bottom: 1rem; +} + +.next-step-item .btn { + margin-top: auto; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .features-grid, + .next-steps-grid { + grid-template-columns: 1fr; + } +} + +#testRequestResponse { + margin-top: 0.5rem; + border-radius: 4px; + font-family: monospace; + /* dark background color */ + background-color: #213547; + color: #f9fafb; + padding: 0.5rem; + display: none; +} + +#testRequestResponse .error { + /* red color that looks good on dark background */ + color: #ff7f7f; +} + +.docs-link { + display: none; + float: right; + margin: 5px 0; +} + +.docs-link a { + color: #3b82f6; +} \ No newline at end of file diff --git a/src/public/styles/tabs.css b/src/public/styles/tabs.css new file mode 100644 index 000000000..951765321 --- /dev/null +++ b/src/public/styles/tabs.css @@ -0,0 +1,73 @@ +/* Tabs styles */ +.tabs-container { + display: flex; + background-color: #f4f4f5; + padding: 0.25rem; + border-radius: 6px; + color: rgb(113, 113, 122); +} + +.tab-button:hover { + color: rgb(9, 9, 11); +} + +.tab-button.active { + color: rgb(9, 9, 11); + /* border-bottom-color: #3b82f6; */ + background-color: white; + font-weight: 500; + box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px; +} + +.tabs-container .tab-button { + min-width: 100px; + padding: 0.3rem 0.875rem; +} + +/* Tab content styles */ +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +.main-tab-content { + display: none; +} + +.main-tab-content.active { + display: block; +} + +.tab-button { + padding: 0.5rem 1rem; + background: none; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + color: #6b7280; + transition: all 0.3s ease; + border-radius: 0.375rem; + /* margin-right: 0.5rem; */ +} + +.tabs { + display: flex; + border-bottom: 1px solid #d1d5db; + margin-bottom: 1rem; +} + +.tab { + padding: 0.5rem 1rem; + cursor: pointer; + border-bottom: 2px solid transparent; +} + +.tab.active { + border-bottom-color: #3b82f6; + font-weight: bold; +} \ No newline at end of file diff --git a/src/services/realtimeLlmEventParser.ts b/src/services/realtimeLlmEventParser.ts new file mode 100644 index 000000000..88415cc87 --- /dev/null +++ b/src/services/realtimeLlmEventParser.ts @@ -0,0 +1,160 @@ +import { Context } from 'hono'; + +export class RealtimeLlmEventParser { + private sessionState: any; + + constructor() { + this.sessionState = { + sessionDetails: null, + conversation: { + items: new Map