Update rokcet dev live chat code

This commit is contained in:
Lisa Ops
2026-08-18 05:20:04 +07:00
parent 7fd959bbff
commit 404e462505
14 changed files with 1247 additions and 677 deletions

View File

@@ -68,3 +68,10 @@ VERCEL_NEON_REGION=""
# Optional JSON object for any product-specific Marketplace metadata required by Neon/Vercel.
VERCEL_NEON_RESOURCE_METADATA_JSON=""
VERCEL_NEON_RESOURCE_PREFIX="rokcet"
# Optional live LLM provider credentials. A user-entered key in Models & Keys overrides
# the matching server env value for that request. Missing credentials now fail visibly;
# Rokcet never substitutes a simulated provider response.
OPENAI_API_KEY=""
ANTHROPIC_API_KEY=""
GROQ_API_KEY=""

View File

@@ -8,6 +8,14 @@ Rokcet is an AI application workspace for generating, inspecting, and deploying
2. Copy `.env.example` to `.env` and configure the services you want to use.
3. Start Rokcet: `npm run dev`
## Live workspace chat
Rokcet chat is strict about provider execution: it sends the current session's user/assistant history with each turn, streams provider output over `/api/chatgpt/execute/stream`, and never replaces a missing key, timeout, provider error, or unsupported model with a fabricated successful response. The model selector dispatches to the matching Google Gemini, OpenAI, Anthropic, or Groq API instead of routing every choice through Gemini.
Configure provider credentials with `GEMINI_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, and/or `GROQ_API_KEY`. A key entered in **Models & Keys** overrides the corresponding server environment value for that browser session/request. **Verify Providers** performs real credential checks; unconfigured or rejected providers are reported as such.
Artifact snapshots are created only when a live provider actually returns filename-addressed code deliverables. Normal prose chat does not create synthetic files or snapshots.
## Buckets.id integration
Rokcet deliberately keeps the two Buckets MCP/runtime responsibilities separate:

View File

@@ -1,21 +1,19 @@
import { GoogleGenAI } from '@google/genai';
let aiClient: GoogleGenAI | null = null;
export function getGeminiClient(): GoogleGenAI {
if (!aiClient) {
const apiKey = process.env.GEMINI_API_KEY;
/**
* Legacy helper kept for callers outside the live chat path. It is intentionally strict:
* Rokcet must never instantiate a fake/dummy Gemini client or convert a missing key into
* a simulated successful response.
*/
export function getGeminiClient(apiKeyOverride?: string): GoogleGenAI {
const apiKey = apiKeyOverride?.trim() || process.env.GEMINI_API_KEY?.trim();
if (!apiKey) {
console.warn('GEMINI_API_KEY is missing. Falling back to structured response generator.');
throw new Error('GEMINI_API_KEY is not configured.');
}
aiClient = new GoogleGenAI({
apiKey: apiKey || 'dummy_key',
return new GoogleGenAI({
apiKey,
httpOptions: {
headers: {
'User-Agent': 'aistudio-build',
},
headers: { 'User-Agent': 'rokcet.dev' },
},
});
}
return aiClient;
}

View File

@@ -1,67 +1,172 @@
import { randomUUID } from 'node:crypto';
import { Router } from 'express';
import { generateMultiModelResponses } from '../services/gemini';
import {
generateMultiModelResponses,
verifyProviders,
type ConversationMessage,
type ProviderApiKeys,
} from '../services/llmProviders';
const router = Router();
router.post('/chatgpt/execute', async (req, res) => {
try {
function parseRequest(body: any): {
prompt: string;
history: ConversationMessage[];
referencedArtifacts: any[];
projectName: string;
selectedModels: string[];
effectiveKeys: ProviderApiKeys;
} {
const {
prompt,
history = [],
credentials,
referencedArtifacts = [],
projectName = 'Default Project',
models = [],
model,
apiKeys = {},
} = req.body;
} = body || {};
if (typeof prompt !== 'string' || !prompt.trim()) throw new Error('A non-empty prompt is required.');
// Support both models array and single model parameter
let selectedModels: string[] = [];
if (Array.isArray(models) && models.length > 0) {
selectedModels = models;
} else if (model) {
selectedModels = [model];
} else if (credentials?.selectedModels && credentials.selectedModels.length > 0) {
selectedModels = credentials.selectedModels;
} else if (credentials?.mainModel) {
selectedModels = [credentials.mainModel];
} else {
selectedModels = ['gemini-3.7-flash'];
}
if (Array.isArray(models) && models.length > 0) selectedModels = models.filter((item) => typeof item === 'string' && item.trim());
else if (typeof model === 'string' && model.trim()) selectedModels = [model.trim()];
else if (Array.isArray(credentials?.selectedModels) && credentials.selectedModels.length > 0) selectedModels = credentials.selectedModels;
else if (credentials?.mainModel) selectedModels = [credentials.mainModel];
else selectedModels = ['gemini-3.7-flash'];
const effectiveKeys = {
const configuredMainModel = typeof credentials?.mainModel === 'string' ? credentials.mainModel.trim() : '';
if (configuredMainModel && selectedModels.includes(configuredMainModel)) {
selectedModels = [configuredMainModel, ...selectedModels.filter((item) => item !== configuredMainModel)];
}
selectedModels = [...new Set(selectedModels)].slice(0, 4);
const cleanHistory: ConversationMessage[] = Array.isArray(history)
? history
.filter((message) => message && (message.role === 'user' || message.role === 'assistant'))
.map((message) => ({ role: message.role, content: String(message.content || '') }))
: [];
return {
prompt: prompt.trim(),
history: cleanHistory,
referencedArtifacts: Array.isArray(referencedArtifacts) ? referencedArtifacts : [],
projectName: String(projectName || 'Default Project'),
selectedModels,
effectiveKeys: {
geminiApiKey: apiKeys.geminiApiKey || credentials?.apiKeys?.geminiApiKey,
openaiApiKey: apiKeys.openaiApiKey || credentials?.apiKeys?.openaiApiKey,
anthropicApiKey: apiKeys.anthropicApiKey || credentials?.apiKeys?.anthropicApiKey,
groqApiKey: apiKeys.groqApiKey || credentials?.apiKeys?.groqApiKey,
},
};
}
const { variants, primaryContent, primaryDeliverables, primaryLogs } =
await generateMultiModelResponses(
prompt,
projectName,
referencedArtifacts,
selectedModels,
effectiveKeys
router.post('/chatgpt/execute', async (req, res) => {
try {
const parsed = parseRequest(req.body);
const result = await generateMultiModelResponses(
parsed.prompt,
parsed.projectName,
parsed.referencedArtifacts,
parsed.selectedModels,
parsed.effectiveKeys,
parsed.history
);
res.json({
success: true,
response: primaryContent,
variants,
logs: primaryLogs,
deliverables: primaryDeliverables,
modelsUsed: selectedModels,
sessionEngineId: 'engine_' + Math.random().toString(36).substring(2, 9),
response: result.primaryContent,
variants: result.variants,
logs: result.primaryLogs,
deliverables: result.primaryDeliverables,
modelsUsed: result.variants.map((variant) => variant.modelId),
primaryModelId: result.primaryModelId,
modelErrors: result.modelErrors,
sessionEngineId: `engine_${randomUUID()}`,
});
} catch (error: any) {
console.error('Error in /api/chatgpt/execute:', error);
res.status(500).json({
console.error('Error in /api/chatgpt/execute:', error?.message || error);
res.status(502).json({
success: false,
error: error.message || 'Failed to execute LLM Workspace Engine.',
error: error?.message || 'Live provider execution failed.',
});
}
});
export default router;
router.post('/chatgpt/execute/stream', async (req, res) => {
let parsed: ReturnType<typeof parseRequest>;
try {
parsed = parseRequest(req.body);
} catch (error: any) {
return res.status(400).json({ success: false, error: error?.message || 'Invalid chat request.' });
}
const executionId = `engine_${randomUUID()}`;
const abortController = new AbortController();
const onClose = () => abortController.abort(new Error('Client disconnected'));
res.on('close', onClose);
res.status(200);
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache, no-transform');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders?.();
const send = (event: string, data: any) => {
if (res.writableEnded || res.destroyed) return;
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
send('execution.started', { executionId, models: parsed.selectedModels, historyMessages: parsed.history.length });
try {
const result = await generateMultiModelResponses(
parsed.prompt,
parsed.projectName,
parsed.referencedArtifacts,
parsed.selectedModels,
parsed.effectiveKeys,
parsed.history,
abortController.signal,
(modelId, delta) => send('model.delta', { modelId, delta }),
(variant) => send('model.completed', { modelId: variant.modelId, latencyMs: variant.latencyMs }),
(modelId, error) => send('model.failed', { modelId, error: error.message })
);
send('execution.completed', {
success: true,
executionId,
response: result.primaryContent,
variants: result.variants,
logs: result.primaryLogs,
deliverables: result.primaryDeliverables,
modelsUsed: result.variants.map((variant) => variant.modelId),
primaryModelId: result.primaryModelId,
modelErrors: result.modelErrors,
});
} catch (error: any) {
if (!abortController.signal.aborted) {
console.error('Streaming LLM execution failed:', error?.message || error);
send('execution.failed', { success: false, executionId, error: error?.message || 'Live provider execution failed.' });
}
} finally {
res.off('close', onClose);
if (!res.writableEnded && !res.destroyed) res.end();
}
});
router.post('/chatgpt/providers/test', async (req, res) => {
try {
const apiKeys = req.body?.apiKeys || {};
const results = await verifyProviders(apiKeys);
res.json({ success: true, results });
} catch (error: any) {
res.status(500).json({ success: false, error: error?.message || 'Provider verification failed.' });
}
});
export default router;

View File

@@ -1,335 +1,14 @@
import { getGeminiClient } from '../config';
export interface GenerateModelOptions {
prompt: string;
projectName: string;
referencedArtifacts: any[];
modelId: string;
apiKeys?: {
geminiApiKey?: string;
openaiApiKey?: string;
anthropicApiKey?: string;
groqApiKey?: string;
};
}
export function getProviderForModel(modelId: string): { provider: 'google' | 'openai' | 'anthropic' | 'groq'; name: string } {
if (modelId.startsWith('gemini-')) {
return { provider: 'google', name: 'Google Gemini' };
}
if (modelId.startsWith('gpt-') || modelId.startsWith('o1') || modelId.startsWith('o3')) {
return { provider: 'openai', name: 'OpenAI' };
}
if (modelId.startsWith('claude-')) {
return { provider: 'anthropic', name: 'Anthropic' };
}
if (modelId.startsWith('llama-') || modelId.startsWith('deepseek-')) {
return { provider: 'groq', name: 'Groq Cloud' };
}
return { provider: 'google', name: 'Google Gemini' };
}
function getNextWeekDaysFormatted(): { dateStr: string; dayName: string; slots: string[] }[] {
const days = [];
const now = new Date();
// Find next Monday
const dayOfWeek = now.getDay();
const daysUntilNextMonday = ((1 + 7 - dayOfWeek) % 7) || 7;
const nextMonday = new Date(now);
nextMonday.setDate(now.getDate() + daysUntilNextMonday);
const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
for (let i = 0; i < 5; i++) {
const d = new Date(nextMonday);
d.setDate(nextMonday.getDate() + i);
const dateStr = d.toISOString().split('T')[0];
days.push({
dateStr,
dayName: dayNames[i],
slots: ['09:00 UTC', '10:30 UTC', '13:00 UTC', '14:30 UTC', '16:00 UTC'],
});
}
return days;
}
export async function generateSingleModelVariant(options: GenerateModelOptions): Promise<{
id: string;
modelId: string;
provider: 'google' | 'openai' | 'anthropic' | 'groq';
modelName: string;
content: string;
logs: any[];
deliverablesExtracted?: any[];
latencyMs: number;
}> {
const startTime = Date.now();
const { prompt, projectName, referencedArtifacts, modelId, apiKeys } = options;
const { provider, name: providerName } = getProviderForModel(modelId);
const logs: any[] = [];
const timestamp = () => new Date().toLocaleTimeString();
logs.push({
id: `log-1-${modelId}`,
timestamp: timestamp(),
step: 'Initializing Workspace Context & Artifacts',
status: 'success',
detail: `Loaded ${referencedArtifacts.length} referenced artifact snapshot(s) for project "${projectName}".`,
});
logs.push({
id: `log-2-${modelId}`,
timestamp: timestamp(),
step: `Authenticating ${providerName} Engine (${modelId})`,
status: 'success',
detail: `Selected Provider: ${providerName}. Target Model: ${modelId}.`,
});
logs.push({
id: `log-3-${modelId}`,
timestamp: timestamp(),
step: 'Executing LLM Generation Stream',
status: 'running',
detail: `Dispatching prompt payload to ${modelId}...`,
});
let assistantText = '';
let extractedDeliverables: any[] = [];
const effectiveGeminiKey = apiKeys?.geminiApiKey || process.env.GEMINI_API_KEY;
const isAvailabilityQuery =
/availab|schedule|calendar|slot|next week|appointment|book|reserve|timing/i.test(prompt);
if (effectiveGeminiKey) {
try {
const ai = getGeminiClient();
let artifactContext = '';
if (referencedArtifacts.length > 0) {
artifactContext = `\n\n--- REFERENCED PROJECT ARTIFACTS ---\n`;
for (const art of referencedArtifacts) {
artifactContext += `\nArtifact: ${art.title} (Type: ${art.type}, Version: v${art.versionNumber})\n`;
if (art.files && art.files.length > 0) {
for (const file of art.files.slice(0, 5)) {
artifactContext += `File: ${file.path}\n\`\`\`\n${file.content.slice(0, 1000)}\n\`\`\`\n`;
}
}
}
}
const nextWeekSchedule = getNextWeekDaysFormatted();
const availabilityContext = `\n\n[AUTHORITATIVE SCHEDULING CONTEXT - BUCKETS.ID & ROCKET ENGINE]
- Target Staging Host: https://buckets-id.stble.fi
- Availability Windows: Monday through Friday, 09:00 - 17:00 UTC
- Buffer Policy: 10 minutes pre-session setup, 10 minutes post-session cooldown
- Upcoming Week Availability Schedule:
${nextWeekSchedule.map((d) => ` * ${d.dayName} (${d.dateStr}): ${d.slots.join(', ')}`).join('\n')}
- Active Service Tiers:
1. Executive Strategy & Architecture Review (30 min) — Free Strategy Session
2. Full-Stack Engineering Deep Dive (60 min) — 50 Credits
3. Custom Model & MCP Tool Integration — Custom Scope`;
const systemInstruction = `You are powering an automated web workspace engine for project "${projectName}" using model "${modelId}" (${providerName}).
You have received the user's prompt and the latest referenced project artifacts.
${isAvailabilityQuery ? availabilityContext : ''}
Provide a comprehensive, high-quality response in the persona and technical style of ${modelId} (${providerName}).
If the user is asking about scheduling, booking, or availability for next week or any dates, detail the exact available time slots, booking procedure, and integration options with Buckets.id.
CRITICAL INSTRUCTION FOR CODE DELIVERABLES:
If the user asks for new code, bug fixes, features, or updates to the frontend or backend, YOU MUST include updated file contents in markdown code blocks with clear filenames at the top of the block, like:
\`\`\`tsx filename="src/components/UpdatedComponent.tsx"
// code here...
\`\`\`
or
\`\`\`ts filename="server/api.ts"
// code here...
\`\`\`
If you deliver new or updated files, specify the target artifact type at the end of your response using:
[DELIVERABLE_METADATA: type="frontend" title="Frontend Bundle" summary="Updated component logic for prompt execution"]`;
const generatePromise = ai.models.generateContent({
model: 'gemini-3.7-flash',
contents: prompt + artifactContext,
config: {
systemInstruction,
temperature: 0.7,
},
});
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Model response timed out')), 6000)
);
const response: any = await Promise.race([generatePromise, timeoutPromise]);
assistantText = response.text || '';
if (!assistantText) {
throw new Error('Empty response from model');
}
logs[2].status = 'success';
logs[2].detail = `Received stream output from ${modelId} in ${((Date.now() - startTime) / 1000).toFixed(1)}s.`;
} catch (err: any) {
console.warn(`[Model Execution Warning] ${modelId}:`, err.message);
logs[2].status = 'success';
logs[2].detail = `Generated response for ${modelId} via ${providerName} engine (${((Date.now() - startTime) / 1000).toFixed(1)}s).`;
if (isAvailabilityQuery) {
const nextWeekDays = getNextWeekDaysFormatted();
assistantText = `[${providerName} - ${modelId}]\n\nI have queried the authoritative availability schedule on **https://buckets-id.stble.fi** for next week:\n\n### Open Consultation & Architecture Review Windows:\n\n${nextWeekDays
.map(
(d) =>
`• **${d.dayName} (${d.dateStr})**\n - Available Slots: \`${d.slots.join('` · `')}\``
)
.join('\n\n')}\n\n### Booking Parameters:\n- **Timezone**: UTC (09:00 - 17:00)\n- **Buffer Policy**: 10 min setup / 10 min cooldown between appointments\n- **Confirmation**: HMAC-signed token with instant calendar synchronization\n\nWould you like me to book one of these slots or scaffold a dedicated booking component for your app?`;
} else {
assistantText = `[${providerName} - ${modelId}]\n\nProcessed prompt for project **"${projectName}"** using **${modelId}**.\n\n### Response Overview:\n- Evaluated prompt requirements against project artifacts.\n- Synchronized architecture components.\n\n\`\`\`tsx filename="src/components/UpdatedFeature.tsx"
import React, { useState } from 'react';
export function UpdatedFeature() {
const [active, setActive] = useState(true);
return (
<div className="p-4 bg-zinc-950 border border-zinc-800 rounded-xl text-zinc-100 font-mono">
<h3 className="text-sm font-bold text-white uppercase tracking-wider">Active Module (${modelId})</h3>
<p className="text-xs text-zinc-400 mt-1">Generated and bound to snapshot</p>
<button
onClick={() => setActive(!active)}
className="mt-3 px-3 py-1.5 bg-white text-black text-xs font-bold rounded hover:bg-zinc-200 transition"
>
Status: {active ? 'ACTIVE' : 'IDLE'}
</button>
</div>
);
}
\`\`\`
[DELIVERABLE_METADATA: type="frontend" title="Frontend Codebase" summary="Updated component logic using ${modelId}"]`;
}
}
} else {
logs[2].status = 'success';
logs[2].detail = `Simulated API execution for ${modelId} (${providerName})`;
if (isAvailabilityQuery) {
const nextWeekDays = getNextWeekDaysFormatted();
assistantText = `[${providerName} - ${modelId}]\n\nI have queried the authoritative availability schedule on **https://buckets-id.stble.fi** for next week:\n\n### Open Windows Next Week:\n\n${nextWeekDays
.map(
(d) =>
`• **${d.dayName} (${d.dateStr})**\n - Open Slots: \`${d.slots.join('` · `')}\``
)
.join('\n\n')}\n\nAll slots are subject to automatic 10-minute conflict buffer enforcement.`;
} else {
assistantText = `[${providerName} - ${modelId}]\n\nProcessed prompt for project **"${projectName}"** using model **${modelId}**.`;
}
}
// Parse deliverables metadata tag
const deliverableMatch = assistantText.match(
/\[DELIVERABLE_METADATA:\s*type="([^"]+)"\s*title="([^"]+)"\s*summary="([^"]+)"\]/
);
// Extract code blocks with filenames
const codeBlockRegex = /```(?:\w+)?\s+(?:filename="([^"]+)"|([^\n]+))\n([\s\S]*?)```/g;
let match;
const extractedFiles = [];
while ((match = codeBlockRegex.exec(assistantText)) !== null) {
const filePath = match[1] || match[2] || '';
const fileContent = match[3];
if (
filePath &&
fileContent &&
!filePath.includes('bash') &&
!filePath.includes('json') &&
(filePath.includes('.ts') || filePath.includes('.tsx') || filePath.includes('.js') || filePath.includes('.jsx'))
) {
extractedFiles.push({
path: filePath.trim(),
content: fileContent.trim(),
language: filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? 'typescript' : 'javascript',
});
}
}
if (extractedFiles.length > 0 || deliverableMatch) {
const artType = deliverableMatch ? deliverableMatch[1] : (referencedArtifacts[0]?.type || 'frontend');
const artTitle = deliverableMatch ? deliverableMatch[2] : (referencedArtifacts[0]?.title || 'Updated Deliverable');
const artSummary = deliverableMatch ? deliverableMatch[3] : `Generated code updates from ${modelId}`;
extractedDeliverables.push({
type: artType,
title: artTitle,
summary: artSummary,
files: extractedFiles.length > 0 ? extractedFiles : [
{
path: 'src/outputDeliverable.tsx',
content: assistantText.slice(0, 1000),
language: 'typescript',
}
],
});
}
logs.push({
id: `log-4-${modelId}`,
timestamp: timestamp(),
step: 'Packaging Artifact Snapshot & Output',
status: 'success',
detail: `Extracted ${extractedDeliverables.length} deliverable package(s). Execution time: ${Date.now() - startTime}ms.`,
});
return {
id: `var-${modelId}-${Date.now()}`,
modelId,
provider,
modelName: `${modelId} (${providerName})`,
content: assistantText.replace(/\[DELIVERABLE_METADATA:[^\]]+\]/g, '').trim(),
logs,
deliverablesExtracted: extractedDeliverables,
latencyMs: Date.now() - startTime,
};
}
export async function generateMultiModelResponses(
prompt: string,
projectName: string,
referencedArtifacts: any[],
models: string[],
apiKeys?: any
): Promise<{
variants: any[];
primaryContent: string;
primaryDeliverables: any[];
primaryLogs: any[];
}> {
const targetModels = models && models.length > 0 ? models : ['gemini-3.7-flash'];
// Execute in parallel across requested models
const results = await Promise.all(
targetModels.map((m) =>
generateSingleModelVariant({
prompt,
projectName,
referencedArtifacts,
modelId: m,
apiKeys,
})
)
);
const primary = results[0];
return {
variants: results.map((res, idx) => ({
...res,
isMain: idx === 0,
})),
primaryContent: primary.content,
primaryDeliverables: primary.deliverablesExtracted || [],
primaryLogs: primary.logs || [],
};
}
// Backward-compatible module path. The implementation is now genuinely multi-provider
// and lives in llmProviders.ts; there are no simulated-success fallbacks here.
export {
getProviderForModel,
generateSingleModelVariant,
generateMultiModelResponses,
verifyProviders,
} from './llmProviders';
export type {
GenerateModelOptions,
ModelVariant,
ProviderApiKeys,
ConversationMessage,
} from './llmProviders';

View File

@@ -0,0 +1,648 @@
export type LlmProvider = 'google' | 'openai' | 'anthropic' | 'groq';
export interface ProviderApiKeys {
geminiApiKey?: string;
openaiApiKey?: string;
anthropicApiKey?: string;
groqApiKey?: string;
}
export interface ConversationMessage {
role: 'user' | 'assistant';
content: string;
}
export interface GenerateModelOptions {
prompt: string;
history?: ConversationMessage[];
projectName: string;
referencedArtifacts: any[];
modelId: string;
apiKeys?: ProviderApiKeys;
signal?: AbortSignal;
onDelta?: (delta: string) => void;
}
export interface ModelVariant {
id: string;
modelId: string;
provider: LlmProvider;
modelName: string;
content: string;
logs: any[];
deliverablesExtracted?: any[];
latencyMs: number;
isMain?: boolean;
}
const PROVIDER_NAMES: Record<LlmProvider, string> = {
google: 'Google Gemini',
openai: 'OpenAI',
anthropic: 'Anthropic',
groq: 'Groq Cloud',
};
const API_KEY_ENV: Record<LlmProvider, string> = {
google: 'GEMINI_API_KEY',
openai: 'OPENAI_API_KEY',
anthropic: 'ANTHROPIC_API_KEY',
groq: 'GROQ_API_KEY',
};
const API_KEY_FIELD: Record<LlmProvider, keyof ProviderApiKeys> = {
google: 'geminiApiKey',
openai: 'openaiApiKey',
anthropic: 'anthropicApiKey',
groq: 'groqApiKey',
};
export function getProviderForModel(modelId: string): { provider: LlmProvider; name: string } {
if (modelId.startsWith('gemini-')) return { provider: 'google', name: PROVIDER_NAMES.google };
if (modelId.startsWith('gpt-') || modelId.startsWith('o1') || modelId.startsWith('o3')) {
return { provider: 'openai', name: PROVIDER_NAMES.openai };
}
if (modelId.startsWith('claude-')) return { provider: 'anthropic', name: PROVIDER_NAMES.anthropic };
if (
modelId.startsWith('openai/gpt-oss-') ||
modelId.startsWith('groq/') ||
modelId.startsWith('llama-') ||
modelId.startsWith('meta-llama/') ||
modelId.startsWith('qwen/') ||
modelId.startsWith('deepseek-')
) {
return { provider: 'groq', name: PROVIDER_NAMES.groq };
}
throw new Error(`Unsupported model "${modelId}". Select a model from Rokcet's provider catalog.`);
}
function resolveApiKey(provider: LlmProvider, apiKeys?: ProviderApiKeys): string {
const supplied = apiKeys?.[API_KEY_FIELD[provider]]?.trim();
const envValue = process.env[API_KEY_ENV[provider]]?.trim();
const key = supplied || envValue;
if (!key) {
throw new Error(
`${PROVIDER_NAMES[provider]} is not connected: configure ${API_KEY_ENV[provider]} on the Rokcet server or enter a provider key in Models & Keys.`
);
}
return key;
}
function sanitizeHistory(history: ConversationMessage[] | undefined, prompt: string): ConversationMessage[] {
const cleaned = (Array.isArray(history) ? history : [])
.filter((message) => message && (message.role === 'user' || message.role === 'assistant'))
.map((message) => ({ role: message.role, content: String(message.content || '').trim() }))
.filter((message) => message.content.length > 0);
// Keep enough conversational context for normal workspace usage without allowing an
// unbounded browser payload to grow forever. Preserve the most recent turns first.
const result: ConversationMessage[] = [];
let totalChars = 0;
const maxChars = 180_000;
for (let index = cleaned.length - 1; index >= 0; index -= 1) {
const message = cleaned[index];
if (totalChars + message.content.length > maxChars && result.length > 0) break;
result.unshift(message);
totalChars += message.content.length;
if (result.length >= 60) break;
}
const last = result[result.length - 1];
if (!last || last.role !== 'user' || last.content !== prompt.trim()) {
result.push({ role: 'user', content: prompt.trim() });
}
return result;
}
function buildArtifactContext(referencedArtifacts: any[]): string {
if (!Array.isArray(referencedArtifacts) || referencedArtifacts.length === 0) return '';
let remaining = 140_000;
let output = '\n\nREFERENCED PROJECT ARTIFACTS (user-selected workspace context):\n';
for (const artifact of referencedArtifacts.slice(0, 12)) {
if (remaining <= 0) break;
const heading = `\nArtifact: ${artifact?.title || 'Untitled'} | type=${artifact?.type || 'other'} | version=v${artifact?.versionNumber || 1}\n`;
output += heading;
remaining -= heading.length;
for (const file of (artifact?.files || []).slice(0, 20)) {
if (remaining <= 0) break;
const path = String(file?.path || 'unknown');
const content = String(file?.content || '');
const allowance = Math.min(content.length, 12_000, Math.max(0, remaining - path.length - 40));
if (allowance <= 0) break;
const block = `FILE: ${path}\n\`\`\`\n${content.slice(0, allowance)}\n\`\`\`\n`;
output += block;
remaining -= block.length;
}
}
if (remaining <= 0) output += '\n[Artifact context truncated to workspace safety limit.]\n';
return output;
}
function buildSystemInstruction(projectName: string, modelId: string, providerName: string, artifacts: any[]): string {
return `You are the live Rokcet workspace coding assistant for project "${projectName}".
The selected execution model is "${modelId}" from ${providerName}.
Maintain continuity with the conversation history you receive. Resolve follow-up references such as "that button", "the previous change", or "do the second option" from prior turns instead of treating each prompt as isolated.
Be truthful about external actions. Do not claim that you queried Buckets, deployed an app, changed a database, inspected a URL, or called any other service unless evidence for that action is explicitly present in the supplied context. You have no implicit tools merely because the project mentions them.
When the user requests code changes and you can provide complete updated files, put each changed file in a fenced code block whose opening fence includes filename="path/to/file.ext". Do not invent files merely to make a deliverable appear successful. If you did not generate a real file change, provide a normal answer without deliverable metadata.
If you produce one or more genuine updated files, you may add exactly one metadata marker at the end:
[DELIVERABLE_METADATA: type="frontend|backend|database|docs|config|other" title="Short title" summary="What actually changed"]
${buildArtifactContext(artifacts)}`;
}
function makeTimeoutSignal(parent?: AbortSignal, timeoutMs = 120_000): { signal: AbortSignal; cleanup: () => void } {
const controller = new AbortController();
const onAbort = () => controller.abort(parent?.reason || new Error('Request cancelled'));
if (parent?.aborted) onAbort();
else parent?.addEventListener('abort', onAbort, { once: true });
const timer = setTimeout(() => controller.abort(new Error('Provider request timed out')), timeoutMs);
return {
signal: controller.signal,
cleanup: () => {
clearTimeout(timer);
parent?.removeEventListener('abort', onAbort);
},
};
}
async function providerError(response: Response, providerName: string, modelId: string): Promise<Error> {
let detail = '';
try {
const data: any = await response.json();
detail = data?.error?.message || data?.message || JSON.stringify(data);
} catch {
try { detail = await response.text(); } catch { detail = ''; }
}
detail = String(detail || '').replace(/\s+/g, ' ').trim().slice(0, 600);
return new Error(
`${providerName} rejected model ${modelId} (HTTP ${response.status})${detail ? `: ${detail}` : ''}`
);
}
async function readSse(
response: Response,
onEvent: (eventName: string, data: string) => void
): Promise<void> {
if (!response.body) throw new Error('Provider returned no streaming response body.');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const consumeFrame = (frame: string) => {
if (!frame.trim()) return;
let eventName = '';
const dataLines: string[] = [];
for (const rawLine of frame.split(/\r?\n/)) {
if (rawLine.startsWith('event:')) eventName = rawLine.slice(6).trim();
else if (rawLine.startsWith('data:')) dataLines.push(rawLine.slice(5).trimStart());
}
if (dataLines.length > 0) onEvent(eventName, dataLines.join('\n'));
};
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const frames = buffer.split(/\r?\n\r?\n/);
buffer = frames.pop() || '';
frames.forEach(consumeFrame);
}
buffer += decoder.decode();
if (buffer.trim()) consumeFrame(buffer);
}
async function streamGemini(
modelId: string,
apiKey: string,
history: ConversationMessage[],
systemInstruction: string,
signal?: AbortSignal,
onDelta?: (delta: string) => void
): Promise<string> {
const { signal: timedSignal, cleanup } = makeTimeoutSignal(signal);
try {
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(modelId)}:streamGenerateContent?alt=sse`,
{
method: 'POST',
signal: timedSignal,
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': apiKey,
},
body: JSON.stringify({
systemInstruction: { parts: [{ text: systemInstruction }] },
contents: history.map((message) => ({
role: message.role === 'assistant' ? 'model' : 'user',
parts: [{ text: message.content }],
})),
}),
}
);
if (!response.ok) throw await providerError(response, 'Google Gemini', modelId);
let fullText = '';
await readSse(response, (_eventName, rawData) => {
if (!rawData || rawData === '[DONE]') return;
let payload: any;
try { payload = JSON.parse(rawData); } catch { return; }
if (payload?.error?.message) throw new Error(String(payload.error.message));
const parts = payload?.candidates?.[0]?.content?.parts || [];
for (const part of parts) {
const delta = typeof part?.text === 'string' ? part.text : '';
if (!delta) continue;
fullText += delta;
onDelta?.(delta);
}
});
return fullText;
} finally {
cleanup();
}
}
async function streamOpenAI(
modelId: string,
apiKey: string,
history: ConversationMessage[],
systemInstruction: string,
signal?: AbortSignal,
onDelta?: (delta: string) => void
): Promise<string> {
const { signal: timedSignal, cleanup } = makeTimeoutSignal(signal);
try {
const response = await fetch('https://api.openai.com/v1/responses', {
method: 'POST',
signal: timedSignal,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: modelId,
instructions: systemInstruction,
input: history.map((message) => ({ role: message.role, content: message.content })),
stream: true,
store: false,
}),
});
if (!response.ok) throw await providerError(response, 'OpenAI', modelId);
let fullText = '';
await readSse(response, (eventName, rawData) => {
if (!rawData || rawData === '[DONE]') return;
let payload: any;
try { payload = JSON.parse(rawData); } catch { return; }
const type = payload?.type || eventName;
if (type === 'response.output_text.delta') {
const delta = typeof payload?.delta === 'string' ? payload.delta : '';
if (delta) {
fullText += delta;
onDelta?.(delta);
}
} else if (type === 'error' || type === 'response.failed') {
const message = payload?.error?.message || payload?.response?.error?.message || payload?.message || 'OpenAI streaming response failed.';
throw new Error(String(message));
}
});
return fullText;
} finally {
cleanup();
}
}
async function streamAnthropic(
modelId: string,
apiKey: string,
history: ConversationMessage[],
systemInstruction: string,
signal?: AbortSignal,
onDelta?: (delta: string) => void
): Promise<string> {
const { signal: timedSignal, cleanup } = makeTimeoutSignal(signal);
try {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
signal: timedSignal,
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: modelId,
system: systemInstruction,
messages: history,
max_tokens: 16_384,
stream: true,
}),
});
if (!response.ok) throw await providerError(response, 'Anthropic', modelId);
let fullText = '';
await readSse(response, (eventName, rawData) => {
if (!rawData || rawData === '[DONE]') return;
let payload: any;
try { payload = JSON.parse(rawData); } catch { return; }
const type = payload?.type || eventName;
if (type === 'content_block_delta' && payload?.delta?.type === 'text_delta') {
const delta = typeof payload.delta.text === 'string' ? payload.delta.text : '';
if (delta) {
fullText += delta;
onDelta?.(delta);
}
} else if (type === 'error') {
throw new Error(payload?.error?.message || 'Anthropic streaming response failed.');
}
});
return fullText;
} finally {
cleanup();
}
}
async function streamGroq(
modelId: string,
apiKey: string,
history: ConversationMessage[],
systemInstruction: string,
signal?: AbortSignal,
onDelta?: (delta: string) => void
): Promise<string> {
const { signal: timedSignal, cleanup } = makeTimeoutSignal(signal);
try {
const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
signal: timedSignal,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: modelId,
messages: [{ role: 'system', content: systemInstruction }, ...history],
max_completion_tokens: 16_384,
stream: true,
}),
});
if (!response.ok) throw await providerError(response, 'Groq Cloud', modelId);
let fullText = '';
await readSse(response, (_eventName, rawData) => {
if (!rawData || rawData === '[DONE]') return;
let payload: any;
try { payload = JSON.parse(rawData); } catch { return; }
const delta = payload?.choices?.[0]?.delta?.content;
if (typeof delta === 'string' && delta) {
fullText += delta;
onDelta?.(delta);
}
if (payload?.error?.message) throw new Error(String(payload.error.message));
});
return fullText;
} finally {
cleanup();
}
}
function extractDeliverables(assistantText: string, modelId: string, referencedArtifacts: any[]): any[] {
const metadataMatch = assistantText.match(
/\[DELIVERABLE_METADATA:\s*type="([^"]+)"\s*title="([^"]+)"\s*summary="([^"]+)"\]/
);
const codeBlockRegex = /```(?:[\w.+-]+)?\s+filename="([^"]+)"\n([\s\S]*?)```/g;
const files: any[] = [];
let match: RegExpExecArray | null;
while ((match = codeBlockRegex.exec(assistantText)) !== null) {
const filePath = String(match[1] || '').trim();
const fileContent = String(match[2] || '').trim();
if (!filePath || !fileContent) continue;
files.push({
path: filePath,
content: fileContent,
language: filePath.split('.').pop() || 'text',
});
}
// Never fabricate a file from prose. A deliverable exists only when the live provider
// actually returned at least one filename-addressed code block.
if (files.length === 0) return [];
return [{
type: metadataMatch?.[1] || referencedArtifacts?.[0]?.type || 'other',
title: metadataMatch?.[2] || referencedArtifacts?.[0]?.title || 'Generated Deliverable',
summary: metadataMatch?.[3] || `Live code output from ${modelId}`,
files,
}];
}
export async function generateSingleModelVariant(options: GenerateModelOptions): Promise<ModelVariant> {
const startTime = Date.now();
const { prompt, projectName, referencedArtifacts, modelId, apiKeys, signal, onDelta } = options;
const { provider, name: providerName } = getProviderForModel(modelId);
const apiKey = resolveApiKey(provider, apiKeys);
const history = sanitizeHistory(options.history, prompt);
const systemInstruction = buildSystemInstruction(projectName, modelId, providerName, referencedArtifacts);
const timestamp = () => new Date().toISOString();
const logs: any[] = [
{
id: `context-${modelId}-${startTime}`,
timestamp: timestamp(),
step: 'Conversation context prepared',
status: 'success',
detail: `Loaded ${history.length} live conversation message(s) and ${referencedArtifacts.length} referenced artifact snapshot(s).`,
},
{
id: `provider-${modelId}-${startTime}`,
timestamp: timestamp(),
step: `Connecting to ${providerName}`,
status: 'running',
detail: `Dispatching a live request to the selected model ${modelId}.`,
},
{
id: `stream-${modelId}-${startTime}`,
timestamp: timestamp(),
step: 'Streaming provider response',
status: 'running',
detail: `Waiting for live output from ${providerName}.`,
},
];
let assistantText = '';
try {
if (provider === 'google') {
assistantText = await streamGemini(modelId, apiKey, history, systemInstruction, signal, onDelta);
} else if (provider === 'openai') {
assistantText = await streamOpenAI(modelId, apiKey, history, systemInstruction, signal, onDelta);
} else if (provider === 'anthropic') {
assistantText = await streamAnthropic(modelId, apiKey, history, systemInstruction, signal, onDelta);
} else {
assistantText = await streamGroq(modelId, apiKey, history, systemInstruction, signal, onDelta);
}
if (!assistantText.trim()) throw new Error(`${providerName} returned an empty response for ${modelId}.`);
logs[1].status = 'success';
logs[1].timestamp = timestamp();
logs[1].step = `Live ${providerName} request authenticated`;
logs[1].detail = `${providerName} accepted and completed the selected model request ${modelId}.`;
logs[2].status = 'success';
logs[2].timestamp = timestamp();
logs[2].detail = `Live provider stream completed in ${Date.now() - startTime}ms.`;
} catch (error: any) {
logs[1].status = 'error';
logs[1].timestamp = timestamp();
logs[1].detail = error?.message || `${providerName} connection failed.`;
logs[2].status = 'error';
logs[2].timestamp = timestamp();
logs[2].detail = error?.message || `${providerName} execution failed.`;
throw error;
}
const deliverables = extractDeliverables(assistantText, modelId, referencedArtifacts);
logs.push({
id: `output-${modelId}-${startTime}`,
timestamp: timestamp(),
step: 'Provider output finalized',
status: 'success',
detail: deliverables.length > 0
? `Extracted ${deliverables.reduce((count, item) => count + item.files.length, 0)} genuine generated file(s).`
: 'No filename-addressed code deliverables were returned; no artifact snapshot was fabricated.',
});
return {
id: `var-${modelId}-${Date.now()}`,
modelId,
provider,
modelName: `${modelId} (${providerName})`,
content: assistantText.replace(/\[DELIVERABLE_METADATA:[^\]]+\]/g, '').trim(),
logs,
deliverablesExtracted: deliverables,
latencyMs: Date.now() - startTime,
};
}
export async function generateMultiModelResponses(
prompt: string,
projectName: string,
referencedArtifacts: any[],
models: string[],
apiKeys?: ProviderApiKeys,
history?: ConversationMessage[],
signal?: AbortSignal,
onModelDelta?: (modelId: string, delta: string) => void,
onModelComplete?: (variant: ModelVariant) => void,
onModelError?: (modelId: string, error: Error) => void
): Promise<{
variants: ModelVariant[];
primaryContent: string;
primaryDeliverables: any[];
primaryLogs: any[];
primaryModelId: string;
modelErrors: { modelId: string; error: string }[];
}> {
const targetModels = models && models.length > 0 ? models : ['gemini-3.7-flash'];
const settled = await Promise.allSettled(
targetModels.map(async (modelId) => {
try {
const variant = await generateSingleModelVariant({
prompt,
history,
projectName,
referencedArtifacts,
modelId,
apiKeys,
signal,
onDelta: (delta) => onModelDelta?.(modelId, delta),
});
onModelComplete?.(variant);
return variant;
} catch (error: any) {
const normalized = error instanceof Error ? error : new Error(String(error));
onModelError?.(modelId, normalized);
throw normalized;
}
})
);
const variants: ModelVariant[] = [];
const modelErrors: { modelId: string; error: string }[] = [];
settled.forEach((result, index) => {
if (result.status === 'fulfilled') variants.push(result.value);
else modelErrors.push({ modelId: targetModels[index], error: result.reason?.message || String(result.reason) });
});
if (variants.length === 0) {
const detail = modelErrors.map((item) => `${item.modelId}: ${item.error}`).join(' | ');
throw new Error(detail || 'All selected provider executions failed.');
}
const preferredPrimaryId = targetModels[0];
const primary = variants.find((variant) => variant.modelId === preferredPrimaryId) || variants[0];
const orderedVariants = [primary, ...variants.filter((variant) => variant.id !== primary.id)].map((variant, index) => ({
...variant,
isMain: index === 0,
}));
return {
variants: orderedVariants,
primaryContent: primary.content,
primaryDeliverables: primary.deliverablesExtracted || [],
primaryLogs: primary.logs || [],
primaryModelId: primary.modelId,
modelErrors,
};
}
export async function verifyProviders(apiKeys?: ProviderApiKeys): Promise<Record<LlmProvider, { configured: boolean; ok: boolean; detail: string }>> {
const providers: LlmProvider[] = ['google', 'openai', 'anthropic', 'groq'];
const results = {} as Record<LlmProvider, { configured: boolean; ok: boolean; detail: string }>;
await Promise.all(providers.map(async (provider) => {
let key = '';
try {
key = resolveApiKey(provider, apiKeys);
} catch (error: any) {
results[provider] = { configured: false, ok: false, detail: error.message };
return;
}
const endpoints: Record<LlmProvider, { url: string; headers: Record<string, string> }> = {
google: {
url: 'https://generativelanguage.googleapis.com/v1beta/models?pageSize=1',
headers: { 'x-goog-api-key': key },
},
openai: {
url: 'https://api.openai.com/v1/models',
headers: { Authorization: `Bearer ${key}` },
},
anthropic: {
url: 'https://api.anthropic.com/v1/models?limit=1',
headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' },
},
groq: {
url: 'https://api.groq.com/openai/v1/models',
headers: { Authorization: `Bearer ${key}` },
},
};
const { signal, cleanup } = makeTimeoutSignal(undefined, 15_000);
try {
const response = await fetch(endpoints[provider].url, { headers: endpoints[provider].headers, signal });
if (response.ok) {
results[provider] = { configured: true, ok: true, detail: `${PROVIDER_NAMES[provider]} API credentials validated live.` };
} else {
const error = await providerError(response, PROVIDER_NAMES[provider], 'credential-check');
results[provider] = { configured: true, ok: false, detail: error.message };
}
} catch (error: any) {
results[provider] = { configured: true, ok: false, detail: error?.message || 'Provider verification failed.' };
} finally {
cleanup();
}
}));
return results;
}

View File

@@ -10,15 +10,14 @@ import {
ReferencedArtifactRef,
ActiveTabType,
SnapshotBinding,
ModelResponseVariant,
} from './types';
import {
INITIAL_WORKSPACES,
INITIAL_PROJECTS,
INITIAL_ARTIFACTS,
INITIAL_SESSIONS,
DEFAULT_CREDENTIALS,
INITIAL_SNAPSHOTS,
INITIAL_SESSIONS,
} from './initialData';
import { Sidebar } from './components/Sidebar';
import { MiniSideMenu } from './components/MiniSideMenu';
@@ -94,6 +93,12 @@ function sanitizeAndDeduplicateArtifacts(rawArtifacts: Artifact[]): Artifact[] {
return Array.from(seenKeys.values());
}
function sanitizeSessions(rawSessions: Session[]): Session[] {
if (!Array.isArray(rawSessions)) return [];
const legacyDemoSessionIds = new Set(['sess-buckets-booking', 'sess-cart-feature']);
return rawSessions.filter((session) => session && session.id && !legacyDemoSessionIds.has(session.id));
}
export default function App() {
const [workspaces, setWorkspaces] = useState<Workspace[]>(INITIAL_WORKSPACES);
const [activeWorkspace, setActiveWorkspace] = useState<Workspace>(INITIAL_WORKSPACES[0]);
@@ -105,10 +110,10 @@ export default function App() {
const [selectedArtifactId, setSelectedArtifactId] = useState<string | null>(INITIAL_ARTIFACTS[0]?.id || null);
const [snapshots, setSnapshots] = useState<SnapshotBinding[]>(INITIAL_SNAPSHOTS);
const [activeSnapshotId, setActiveSnapshotId] = useState<string | null>(INITIAL_SNAPSHOTS[0]?.id || null);
const [sessions, setSessions] = useState<Session[]>(INITIAL_SESSIONS);
const [activeSession, setActiveSession] = useState<Session | null>(
INITIAL_SESSIONS.find((sess) => sess.projectId === INITIAL_PROJECTS[0]?.id) || null
);
// Live chat starts empty. Historical sample sessions contained fabricated provider traces and
// are deliberately not mounted into authenticated workspaces.
const [sessions, setSessions] = useState<Session[]>([]);
const [activeSession, setActiveSession] = useState<Session | null>(null);
const [credentials, setCredentials] = useState<LlmEngineConfig>(DEFAULT_CREDENTIALS);
// Authentication is server-authoritative. Browser storage is never used to infer
@@ -149,7 +154,7 @@ export default function App() {
const loadedProjects = loadStorage<Project[]>(key('projects'), defaultProjects).map(bindSampleProjectToWorkspace);
const loadedArtifacts = sanitizeAndDeduplicateArtifacts(loadStorage<Artifact[]>(key('artifacts'), INITIAL_ARTIFACTS));
const loadedSnapshots = loadStorage<SnapshotBinding[]>(key('snapshots'), INITIAL_SNAPSHOTS);
const loadedSessions = loadStorage<Session[]>(key('sessions'), INITIAL_SESSIONS);
const loadedSessions = sanitizeSessions(loadStorage<Session[]>(key('sessions'), []));
const savedWorkspace = loadStorage<Workspace | null>(key('active_workspace'), null);
const nextWorkspace = mergedWorkspaces.find((workspace) => workspace.id === savedWorkspace?.id) || mergedWorkspaces[0];
@@ -470,20 +475,20 @@ export default function App() {
}
};
// Send Prompt & Run Parallel Multi-Model Generation
// Send Prompt through the live provider stream. Conversation history is sent on every turn,
// so follow-up messages remain connected to the active session instead of becoming isolated prompts.
const handleSendMessage = async (
promptText: string,
attachedRefs: { artifactId: string; versionNumber: number }[]
) => {
if (!activeProject) return;
if (!activeProject || !promptText.trim()) return;
// Create session if none active
let currentSess = activeSession;
if (!currentSess) {
currentSess = {
id: 'sess-' + Date.now(),
projectId: activeProject.id,
title: promptText.slice(0, 35) + '...',
title: promptText.slice(0, 35) + (promptText.length > 35 ? '...' : ''),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
referencedArtifacts: attachedRefs,
@@ -493,15 +498,12 @@ export default function App() {
setActiveSession(currentSess);
}
// Build rich payload of referenced artifacts
const referencedDetails: ReferencedArtifactRef[] = [];
const payloadForServer: any[] = [];
attachedRefs.forEach((ref) => {
const art = artifacts.find((a) => a.id === ref.artifactId);
if (art) {
const ver = art.versions.find((v) => v.versionNumber === ref.versionNumber);
if (ver) {
const ver = art?.versions.find((v) => v.versionNumber === ref.versionNumber);
if (!art || !ver) return;
referencedDetails.push({
artifactId: art.id,
versionNumber: ver.versionNumber,
@@ -516,41 +518,68 @@ export default function App() {
versionNumber: ver.versionNumber,
files: ver.files,
});
}
}
});
// Create User Message
const userMsg: ChatMessage = {
id: 'msg-usr-' + Date.now(),
sessionId: currentSess.id,
role: 'user',
content: promptText,
content: promptText.trim(),
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
referencedArtifacts: referencedDetails,
};
// Update session state locally with user message
const updatedUserMessages = [...currentSess.messages, userMsg];
const updatedSessWithUser = { ...currentSess, messages: updatedUserMessages };
setSessions((prev) =>
prev.map((s) => (s.id === currentSess!.id ? updatedSessWithUser : s))
);
const updatedSessWithUser = { ...currentSess, updatedAt: new Date().toISOString(), messages: updatedUserMessages };
setSessions((prev) => prev.map((s) => (s.id === currentSess!.id ? updatedSessWithUser : s)));
setActiveSession(updatedSessWithUser);
setIsExecuting(true);
try {
const selectedModels = credentials.selectedModels?.length > 0
const configuredModels = credentials.selectedModels?.length > 0
? credentials.selectedModels
: [(credentials as any).gptModel || (credentials as any).mainModel || 'gemini-3.7-flash'];
: [credentials.mainModel || 'gemini-3.7-flash'];
const configuredMainModel = credentials.mainModel || configuredModels[0] || 'gemini-3.7-flash';
const selectedModels = configuredModels.includes(configuredMainModel)
? [configuredMainModel, ...configuredModels.filter((modelId) => modelId !== configuredMainModel)]
: [configuredMainModel, ...configuredModels];
const requestedPrimaryModel = selectedModels[0];
const streamingMessageId = 'msg-stream-' + Date.now();
let primaryStreamText = '';
const res = await fetch('/api/chatgpt/execute', {
const mountOrUpdateStream = (content: string) => {
const streamMsg: ChatMessage = {
id: streamingMessageId,
sessionId: currentSess!.id,
role: 'assistant',
content,
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
isExecuting: true,
selectedModelId: requestedPrimaryModel,
};
setSessions((prev) => prev.map((session) => {
if (session.id !== currentSess!.id) return session;
const withoutOldStream = session.messages.filter((message) => message.id !== streamingMessageId);
return { ...session, updatedAt: new Date().toISOString(), messages: [...withoutOldStream, streamMsg] };
}));
setActiveSession((session) => {
if (!session || session.id !== currentSess!.id) return session;
const withoutOldStream = session.messages.filter((message) => message.id !== streamingMessageId);
return { ...session, updatedAt: new Date().toISOString(), messages: [...withoutOldStream, streamMsg] };
});
};
try {
const history = updatedUserMessages
.filter((message) => message.role === 'user' || message.role === 'assistant')
.filter((message) => !message.isExecuting)
.map((message) => ({ role: message.role as 'user' | 'assistant', content: message.content }));
const res = await fetch('/api/chatgpt/execute/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
body: JSON.stringify({
prompt: promptText,
credentials,
prompt: promptText.trim(),
history,
referencedArtifacts: payloadForServer,
projectName: activeProject.name,
models: selectedModels,
@@ -558,22 +587,62 @@ export default function App() {
}),
});
const data = await res.json();
if (!data.success) {
throw new Error(data.error || 'LLM Engine execution failed.');
if (!res.ok || !res.body) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `Live chat request failed (HTTP ${res.status}).`);
}
// Check for extracted deliverables and auto-update Artifacts Vault!
const deliverablesExtractedList: any[] = [];
const primaryDeliverables = data.deliverables || [];
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let completionData: any = null;
const modelFailures: { modelId: string; error: string }[] = [];
const consumeFrame = (frame: string) => {
if (!frame.trim()) return;
let eventName = '';
const dataLines: string[] = [];
frame.split(/\r?\n/).forEach((line) => {
if (line.startsWith('event:')) eventName = line.slice(6).trim();
else if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart());
});
if (dataLines.length === 0) return;
let payload: any;
try { payload = JSON.parse(dataLines.join('\n')); } catch { return; }
if (eventName === 'model.delta' && payload.modelId === requestedPrimaryModel && typeof payload.delta === 'string') {
primaryStreamText += payload.delta;
mountOrUpdateStream(primaryStreamText);
} else if (eventName === 'model.failed') {
modelFailures.push({ modelId: payload.modelId, error: payload.error });
} else if (eventName === 'execution.completed') {
completionData = payload;
} else if (eventName === 'execution.failed') {
throw new Error(payload.error || 'All selected live provider executions failed.');
}
};
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const frames = buffer.split(/\r?\n\r?\n/);
buffer = frames.pop() || '';
frames.forEach(consumeFrame);
}
buffer += decoder.decode();
if (buffer.trim()) consumeFrame(buffer);
if (!completionData?.success) throw new Error('Live provider stream ended before completion.');
const data = completionData;
const deliverablesExtractedList: any[] = [];
const primaryDeliverables = credentials.extractDeliverables === false ? [] : (data.deliverables || []);
if (primaryDeliverables.length > 0) {
primaryDeliverables.forEach((deliv: any) => {
if (!Array.isArray(deliv?.files) || deliv.files.length === 0) return;
const delivType = String(deliv.type || 'other');
const existingArt = artifacts.find(
(a) =>
a.projectId === activeProject.id &&
(a.type === deliv.type || a.title.toLowerCase().includes(deliv.type))
(a) => a.projectId === activeProject.id && (a.type === delivType || a.title.toLowerCase().includes(delivType.toLowerCase()))
);
if (existingArt) {
@@ -582,26 +651,15 @@ export default function App() {
id: 'ver-' + nextVersionNum + '-' + Date.now(),
versionNumber: nextVersionNum,
createdAt: new Date().toISOString(),
summary: deliv.summary || `Updated via LLM prompt`,
summary: deliv.summary || 'Updated via live LLM provider output',
zipFileName: `${existingArt.title.toLowerCase().replace(/\s+/g, '_')}_v${nextVersionNum}.zip`,
files: deliv.files,
createdByPrompt: promptText,
createdByModel: data.primaryModelId,
};
setArtifacts((prev) =>
prev.map((a) => {
if (a.id === existingArt.id) {
return {
...a,
currentVersion: nextVersionNum,
updatedAt: new Date().toISOString(),
versions: [...a.versions, newVer],
};
}
return a;
})
);
setArtifacts((prev) => prev.map((a) => a.id === existingArt.id
? { ...a, currentVersion: nextVersionNum, updatedAt: new Date().toISOString(), versions: [...a.versions, newVer] }
: a));
deliverablesExtractedList.push({
artifactId: existingArt.id,
versionNumber: nextVersionNum,
@@ -610,30 +668,28 @@ export default function App() {
filesCount: deliv.files.length,
});
} else {
const newArtId = 'art-' + Date.now();
const newArtId = 'art-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
const title = String(deliv.title || `${delivType.toUpperCase()} Deliverable`);
const newArt: Artifact = {
id: newArtId,
projectId: activeProject.id,
title: deliv.title || `${deliv.type.toUpperCase()} Deliverable`,
type: deliv.type,
title,
type: delivType as any,
currentVersion: 1,
description: deliv.summary,
updatedAt: new Date().toISOString(),
versions: [
{
versions: [{
id: 'ver-1-' + Date.now(),
versionNumber: 1,
createdAt: new Date().toISOString(),
summary: deliv.summary,
zipFileName: `${deliv.title.toLowerCase().replace(/\s+/g, '_')}_v1.zip`,
summary: deliv.summary || 'Generated by live LLM provider output',
zipFileName: `${title.toLowerCase().replace(/\s+/g, '_')}_v1.zip`,
files: deliv.files,
createdByPrompt: promptText,
},
],
createdByModel: data.primaryModelId,
}],
};
setArtifacts((prev) => [...prev, newArt]);
deliverablesExtractedList.push({
artifactId: newArtId,
versionNumber: 1,
@@ -643,78 +699,96 @@ export default function App() {
});
}
});
}
// Create new bound snapshot in git tree
let generatedSnapshotId: string | undefined;
if (deliverablesExtractedList.length > 0) {
const nextSnapNum = snapshots.length + 1;
generatedSnapshotId = `snap-${Date.now()}`;
const primaryVariant = (data.variants || []).find((variant: any) => variant.modelId === data.primaryModelId);
const boundVersions = projectArtifacts.map((art) => {
const generated = deliverablesExtractedList.find((item) => item.artifactId === art.id);
return { artifactId: art.id, versionNumber: generated?.versionNumber || art.currentVersion };
});
deliverablesExtractedList.forEach((item) => {
if (!boundVersions.some((binding) => binding.artifactId === item.artifactId)) {
boundVersions.push({ artifactId: item.artifactId, versionNumber: item.versionNumber });
}
});
const boundArtifactVersions = boundVersions.reduce((acc, binding) => {
acc[binding.artifactId] = binding.versionNumber;
return acc;
}, {} as Record<string, number>);
const newSnapshot: SnapshotBinding = {
id: `snap-${Date.now()}`,
id: generatedSnapshotId,
versionLabel: `v${nextSnapNum}`,
versionTag: `v${nextSnapNum}.0.0`,
commitHash: Math.random().toString(36).substring(2, 9),
commitMessage: promptText.length > 40 ? promptText.slice(0, 40) + '...' : promptText,
createdAt: new Date().toISOString(),
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
createdByModel: selectedModels[0],
generatedByModel: selectedModels[0],
provider: 'google',
createdByModel: data.primaryModelId,
generatedByModel: data.primaryModelId,
provider: primaryVariant?.provider || 'google',
branchName: 'main',
isMainBranch: true,
author: selectedModels[0],
boundArtifactVersions: projectArtifacts.reduce((acc, art) => {
const matchDeliv = deliverablesExtractedList.find((d) => d.artifactId === art.id);
acc[art.id] = matchDeliv ? matchDeliv.versionNumber : art.currentVersion;
return acc;
}, {} as Record<string, number>),
boundVersions: projectArtifacts.map((art) => {
const matchDeliv = deliverablesExtractedList.find((d) => d.artifactId === art.id);
return {
artifactId: art.id,
versionNumber: matchDeliv ? matchDeliv.versionNumber : art.currentVersion,
author: data.primaryModelId,
boundArtifactVersions,
boundVersions,
summary: `Snapshot generated from ${deliverablesExtractedList.length} real code deliverable(s) via ${data.primaryModelId}`,
};
}),
summary: `Bound snapshot generated via ${selectedModels.join(', ')}`,
};
setSnapshots((prev) => [newSnapshot, ...prev.map((s) => ({ ...s, isMainBranch: false }))]);
setSnapshots((prev) => [newSnapshot, ...prev.map((snapshot) => ({ ...snapshot, isMainBranch: false }))]);
setActiveSnapshotId(newSnapshot.id);
}
const warningLogs = [...(data.modelErrors || modelFailures)].map((failure: any, index: number) => ({
id: `provider-warning-${Date.now()}-${index}`,
timestamp: new Date().toISOString(),
step: `Provider execution failed: ${failure.modelId}`,
status: 'warning' as const,
detail: failure.error,
}));
// Server variants contain raw generated-file candidates. The chat UI's
// deliverablesExtracted field means files that were actually committed into the
// Artifact Vault, so only expose the primary variant's real vault bindings here.
const normalizedVariants = (data.variants || []).map((variant: any) => ({
...variant,
deliverablesExtracted:
variant.modelId === data.primaryModelId ? deliverablesExtractedList : [],
}));
// Create Assistant Message with Variants
const assistantMsg: ChatMessage = {
id: 'msg-ast-' + Date.now(),
sessionId: currentSess.id,
role: 'assistant',
content: data.response,
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
logs: data.logs,
logs: [...(data.logs || []), ...warningLogs],
deliverablesExtracted: deliverablesExtractedList,
variants: data.variants || [],
variants: normalizedVariants,
activeVariantIndex: 0,
selectedModelId: data.primaryModelId,
snapshotId: generatedSnapshotId,
chatgptChatId: data.executionId,
};
const finalMessages = [...updatedUserMessages, assistantMsg];
const finalSess = { ...currentSess, messages: finalMessages };
setSessions((prev) =>
prev.map((s) => (s.id === currentSess!.id ? finalSess : s))
);
const finalSess = { ...currentSess, updatedAt: new Date().toISOString(), messages: finalMessages };
setSessions((prev) => prev.map((s) => (s.id === currentSess!.id ? finalSess : s)));
setActiveSession(finalSess);
} catch (err: any) {
console.error('Error executing Multi-Model Workspace Engine:', err);
console.error('Live provider chat execution failed:', err);
const errorMsg: ChatMessage = {
id: 'msg-err-' + Date.now(),
sessionId: currentSess.id,
role: 'assistant',
content: `⚠️ **Multi-Model Engine Alert**: ${err.message}\n\nPlease check your provider API keys and model settings.`,
role: 'system',
content: `⚠️ **Live provider error**: ${err.message}\n\nNo mocked response was substituted. Check the selected provider key/model and try again.`,
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
};
const errorMessages = [...updatedUserMessages, errorMsg];
const errorSess = { ...currentSess, messages: errorMessages };
setSessions((prev) =>
prev.map((s) => (s.id === currentSess!.id ? errorSess : s))
);
const errorSess = { ...currentSess, updatedAt: new Date().toISOString(), messages: errorMessages };
setSessions((prev) => prev.map((s) => (s.id === currentSess!.id ? errorSess : s)));
setActiveSession(errorSess);
} finally {
setIsExecuting(false);

View File

@@ -91,14 +91,20 @@ export function ChatSessionView({
const promptInputRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (credentials.autoAttachArtifacts === false) {
setSelectedAttachmentRefs([]);
return;
}
if (artifacts.length > 0) {
const defaultLatest = artifacts.map((art) => ({
artifactId: art.id,
versionNumber: art.currentVersion,
}));
setSelectedAttachmentRefs(defaultLatest);
} else {
setSelectedAttachmentRefs([]);
}
}, [artifacts, project.id]);
}, [artifacts, project.id, credentials.autoAttachArtifacts]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@@ -163,7 +169,7 @@ export function ChatSessionView({
key={m}
className="px-2 py-0.5 bg-zinc-100 border border-zinc-300 rounded text-black text-[10px] font-black uppercase flex items-center gap-1 shrink-0"
>
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
<span className="w-1.5 h-1.5 rounded-full bg-zinc-500" />
{m}
</span>
))}
@@ -185,20 +191,20 @@ export function ChatSessionView({
</button>
<button
type="button"
onClick={() => quickSwitchPreset(['gemini-3.7-flash', 'gpt-4o'])}
onClick={() => quickSwitchPreset(['gemini-3.7-flash', 'gpt-5.6'])}
className={`px-2 py-0.5 rounded text-[10px] font-black uppercase border transition cursor-pointer ${
activeModelsList.length === 2 &&
activeModelsList.includes('gemini-3.7-flash') &&
activeModelsList.includes('gpt-4o')
activeModelsList.includes('gpt-5.6')
? 'bg-black text-white border-black'
: 'bg-white hover:bg-zinc-100 text-zinc-800 border-zinc-300'
}`}
>
Gemini + GPT-4o
Gemini + GPT-5.6
</button>
<button
type="button"
onClick={() => quickSwitchPreset(['gemini-3.7-flash', 'gpt-4o', 'claude-3-5-sonnet'])}
onClick={() => quickSwitchPreset(['gemini-3.7-flash', 'gpt-5.6', 'claude-sonnet-5'])}
className={`px-2 py-0.5 rounded text-[10px] font-black uppercase border transition cursor-pointer ${
activeModelsList.length >= 3
? 'bg-black text-white border-black'
@@ -362,20 +368,20 @@ export function ChatSessionView({
desc: 'Ultra-fast multimodal reasoning & code synthesis.',
},
{
id: 'gpt-4o',
name: 'OpenAI GPT-4o',
id: 'gpt-5.6',
name: 'OpenAI GPT-5.6 Sol',
provider: 'OpenAI',
desc: 'State of the art code synthesis & cross-framework patterns.',
},
{
id: 'claude-3-5-sonnet',
name: 'Claude 3.5 Sonnet',
id: 'claude-sonnet-5',
name: 'Claude Sonnet 5',
provider: 'Anthropic',
desc: 'High precision TypeScript architectures & refactoring.',
},
{
id: 'llama-3.3-70b-versatile',
name: 'LLaMA 3.3 70B',
id: 'openai/gpt-oss-120b',
name: 'GPT-OSS 120B',
provider: 'Groq',
desc: 'Sub-second inference speed for rapid feedback cycles.',
},
@@ -461,21 +467,21 @@ export function ChatSessionView({
</button>
<button
type="button"
onClick={() => quickSwitchPreset(['gemini-3.7-flash', 'gpt-4o'])}
onClick={() => quickSwitchPreset(['gemini-3.7-flash', 'gpt-5.6'])}
className={`px-2 py-0.5 rounded text-[10px] font-black uppercase border transition cursor-pointer ${
activeModelsList.length === 2 &&
activeModelsList.includes('gemini-3.7-flash') &&
activeModelsList.includes('gpt-4o')
activeModelsList.includes('gpt-5.6')
? 'bg-black text-white border-black'
: 'bg-white hover:bg-zinc-100 text-zinc-800 border-zinc-300'
}`}
>
Gemini + GPT-4o
Gemini + GPT-5.6
</button>
<button
type="button"
onClick={() =>
quickSwitchPreset(['gemini-3.7-flash', 'gpt-4o', 'claude-3-5-sonnet'])
quickSwitchPreset(['gemini-3.7-flash', 'gpt-5.6', 'claude-sonnet-5'])
}
className={`px-2 py-0.5 rounded text-[10px] font-black uppercase border transition cursor-pointer ${
activeModelsList.length >= 3
@@ -512,7 +518,7 @@ export function ChatSessionView({
</h2>
</div>
<p className="text-zinc-600 text-[11px] font-bold">
Enter API keys for your selected providers. Keys are stored locally in your browser.
Enter API keys for your selected providers. Keys are used only for live provider requests from the Rokcet server process and are never inserted into chat content.
</p>
</div>
@@ -553,7 +559,7 @@ export function ChatSessionView({
<div className="flex items-center justify-between">
<span className="text-[10px] font-black uppercase text-black flex items-center gap-1">
<Key className="w-3 h-3 text-zinc-600" />
<span>OpenAI (GPT-4o) Key</span>
<span>OpenAI API Key</span>
</span>
<span className="text-[9px] text-zinc-400 font-bold uppercase">
sk-...
@@ -613,7 +619,7 @@ export function ChatSessionView({
<div className="flex items-center justify-between">
<span className="text-[10px] font-black uppercase text-black flex items-center gap-1">
<Key className="w-3 h-3 text-zinc-600" />
<span>Groq (LLaMA 3.3) Key</span>
<span>Groq API Key</span>
</span>
<span className="text-[9px] text-zinc-400 font-bold uppercase">
gsk_...
@@ -846,7 +852,7 @@ export function ChatSessionView({
<span>TRY SWITCHER & MAIN BRANCH DEMO:</span>
</span>
<span className="text-[10px] font-mono text-zinc-500 font-bold">
Model: {demoVariantIndex === 0 ? 'gemini-3.7-flash' : 'gpt-4o'}
Model: {demoVariantIndex === 0 ? 'gemini-3.7-flash' : 'gpt-5.6'}
</span>
</div>
@@ -881,7 +887,7 @@ export function ChatSessionView({
</button>
</div>
<span className="text-[11px] font-black uppercase text-zinc-900 bg-zinc-100 px-2 py-0.5 rounded border border-zinc-200">
{demoVariantIndex === 0 ? 'gemini-3.7-flash' : 'gpt-4o'}
{demoVariantIndex === 0 ? 'gemini-3.7-flash' : 'gpt-5.6'}
</span>
</div>

View File

@@ -73,7 +73,7 @@ export function MapView({
)
);
// Ensure default models exist if snapshots are sparse
const defaultModelsList = ['gemini-3.7-flash', 'gpt-4o', 'claude-3-5-sonnet'];
const defaultModelsList = ['gemini-3.7-flash', 'gpt-5.6', 'claude-sonnet-5'];
defaultModelsList.forEach((m) => {
if (!allModels.includes(m) && allModels.length < 3) {
allModels.push(m);

View File

@@ -3,7 +3,6 @@ import { LlmEngineConfig, LlmProvider } from '../types';
import { LLM_PROVIDERS, AVAILABLE_MODELS } from '../data/modelsCatalog';
import {
Sliders,
Check,
X,
Key,
Layers,
@@ -52,7 +51,9 @@ export function SettingsModal({
const [extractDeliverables, setExtractDeliverables] = useState(
credentials.extractDeliverables ?? true
);
const [testSuccess, setTestSuccess] = useState(false);
const [isTestingProviders, setIsTestingProviders] = useState(false);
const [providerTestResults, setProviderTestResults] = useState<Record<string, { configured: boolean; ok: boolean; detail: string }> | null>(null);
const [providerTestError, setProviderTestError] = useState('');
const toggleModelSelection = (modelId: string) => {
if (selectedModels.includes(modelId)) {
@@ -82,9 +83,24 @@ export function SettingsModal({
onClose();
};
const handleTestKeys = () => {
setTestSuccess(true);
setTimeout(() => setTestSuccess(false), 3000);
const handleTestKeys = async () => {
setIsTestingProviders(true);
setProviderTestError('');
setProviderTestResults(null);
try {
const response = await fetch('/api/chatgpt/providers/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKeys }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok || !data.success) throw new Error(data.error || `Provider verification failed (HTTP ${response.status}).`);
setProviderTestResults(data.results || {});
} catch (error: any) {
setProviderTestError(error?.message || 'Provider verification failed.');
} finally {
setIsTestingProviders(false);
}
};
const getIcon = (iconName: string) => {
@@ -172,7 +188,7 @@ export function SettingsModal({
{currentProviderInfo.name} API KEY
</span>
<span className="text-[10px] text-zinc-500">
{apiKeys[currentProviderInfo.apiKeyField] ? '✓ Key Configured' : 'Using Environment Default'}
{apiKeys[currentProviderInfo.apiKeyField] ? '✓ Browser key configured for this session' : 'Server environment checked at runtime'}
</span>
</div>
<input
@@ -296,11 +312,35 @@ export function SettingsModal({
</label>
</div>
{/* Test connection status alert */}
{testSuccess && (
<div className="p-2.5 bg-zinc-100 text-black border border-black font-black flex items-center gap-2 uppercase">
<Check className="w-4 h-4 text-emerald-600" />
<span>Provider Credentials & Multi-Model Engine Validated!</span>
{/* Live provider verification results. No optimistic/mock success state. */}
{providerTestError && (
<div className="p-2.5 bg-red-50 text-red-800 border border-red-300 font-bold text-[10px]">
{providerTestError}
</div>
)}
{providerTestResults && (
<div className="border border-zinc-300 divide-y divide-zinc-200 bg-white">
{LLM_PROVIDERS.map((provider) => {
const result = providerTestResults[provider.id];
if (!result) return null;
return (
<div key={provider.id} className="p-2.5 flex items-start justify-between gap-3">
<div>
<div className="text-[10px] font-black uppercase text-zinc-900">{provider.name}</div>
<div className="text-[9px] font-medium text-zinc-500 mt-0.5">{result.detail}</div>
</div>
<span className={`shrink-0 px-2 py-0.5 text-[9px] font-black uppercase border ${
result.ok
? 'bg-emerald-50 text-emerald-800 border-emerald-300'
: result.configured
? 'bg-red-50 text-red-800 border-red-300'
: 'bg-zinc-100 text-zinc-500 border-zinc-300'
}`}>
{result.ok ? 'LIVE' : result.configured ? 'FAILED' : 'NOT CONFIGURED'}
</span>
</div>
);
})}
</div>
)}
@@ -308,10 +348,11 @@ export function SettingsModal({
<button
type="button"
onClick={handleTestKeys}
className="px-3 py-2 bg-zinc-100 hover:bg-zinc-200 text-black font-black uppercase border border-zinc-300 flex items-center gap-1.5 cursor-pointer"
disabled={isTestingProviders}
className="px-3 py-2 bg-zinc-100 hover:bg-zinc-200 disabled:opacity-50 text-black font-black uppercase border border-zinc-300 flex items-center gap-1.5 cursor-pointer"
>
<Zap className="w-3.5 h-3.5 text-black" />
<span>Verify Providers</span>
<Zap className={`w-3.5 h-3.5 text-black ${isTestingProviders ? 'animate-pulse' : ''}`} />
<span>{isTestingProviders ? 'Verifying Live...' : 'Verify Providers'}</span>
</button>
<div className="flex items-center gap-2">

View File

@@ -17,7 +17,7 @@ export const LLM_PROVIDERS: ProviderInfo[] = [
badge: 'Gemini',
apiKeyField: 'geminiApiKey',
placeholderKey: 'AIzaSy...',
description: 'Native Google GenAI models for high efficiency and long context window.',
description: 'Google Gemini API. Rokcet streams directly from the selected Gemini model.',
icon: 'Sparkles',
},
{
@@ -26,7 +26,7 @@ export const LLM_PROVIDERS: ProviderInfo[] = [
badge: 'GPT',
apiKeyField: 'openaiApiKey',
placeholderKey: 'sk-proj-...',
description: 'Industry-standard GPT & reasoning models.',
description: 'OpenAI Responses API with live server-sent streaming.',
icon: 'Cpu',
},
{
@@ -35,7 +35,7 @@ export const LLM_PROVIDERS: ProviderInfo[] = [
badge: 'Claude',
apiKeyField: 'anthropicApiKey',
placeholderKey: 'sk-ant-api...',
description: 'Claude 3.5 models optimized for complex software development.',
description: 'Anthropic Messages API with live content-block streaming.',
icon: 'Bot',
},
{
@@ -44,118 +44,115 @@ export const LLM_PROVIDERS: ProviderInfo[] = [
badge: 'Groq',
apiKeyField: 'groqApiKey',
placeholderKey: 'gsk_...',
description: 'Ultra-low latency inference for open weights models.',
description: 'GroqCloud production text models through the live Chat Completions stream.',
icon: 'Zap',
},
];
export const AVAILABLE_MODELS: ModelDescriptor[] = [
// Google Gemini
{
id: 'gemini-3.7-flash',
name: 'Gemini 3.7 Flash',
provider: 'google',
providerName: 'Google Gemini',
description: 'Ultra-fast multimodal execution & high coding accuracy (Default)',
badge: 'Fast & Smart',
description: 'Current stable Gemini Flash model for coding and agentic workflows.',
badge: 'Stable',
isPopular: true,
},
{
id: 'gemini-3.6-flash',
name: 'Gemini 3.6 Flash',
provider: 'google',
providerName: 'Google Gemini',
description: 'Previous-generation stable Flash model.',
badge: 'Stable',
},
{
id: 'gemini-3.1-pro-preview',
name: 'Gemini 3.1 Pro',
provider: 'google',
providerName: 'Google Gemini',
description: 'Deep STEM, complex architecture & high-level reasoning',
badge: 'Deep Reasoning',
isPopular: true,
description: 'Preview Pro model for complex reasoning and coding.',
badge: 'Preview',
},
{
id: 'gemini-3.1-flash-lite',
name: 'Gemini 3.1 Flash Lite',
name: 'Gemini 3.1 Flash-Lite',
provider: 'google',
providerName: 'Google Gemini',
description: 'Lightweight model engineered for instantaneous feedback',
description: 'Stable lightweight Gemini model for low-latency work.',
badge: 'Low Latency',
},
{
id: 'gemini-flash-latest',
name: 'Gemini Flash Latest',
provider: 'google',
providerName: 'Google Gemini',
description: 'Latest Google Flash release track',
badge: 'Latest',
},
// OpenAI
{
id: 'gpt-4o',
name: 'GPT-4o (Omni)',
id: 'gpt-5.6',
name: 'GPT-5.6 Sol',
provider: 'openai',
providerName: 'OpenAI',
description: 'Omni reasoning model for code generation and refactoring',
badge: 'Omni Reasoning',
description: 'OpenAI flagship alias for complex professional work and coding.',
badge: 'Frontier',
isPopular: true,
},
{
id: 'gpt-4-turbo',
name: 'GPT-4 Turbo',
id: 'gpt-5.6-terra',
name: 'GPT-5.6 Terra',
provider: 'openai',
providerName: 'OpenAI',
description: 'High-capacity code & system architecture generation',
badge: 'Turbo',
description: 'Balances capability, latency, and cost.',
badge: 'Balanced',
},
{
id: 'o1-mini',
name: 'o1-mini',
id: 'gpt-5.6-luna',
name: 'GPT-5.6 Luna',
provider: 'openai',
providerName: 'OpenAI',
description: 'Specialized STEM & algorithmic code synthesis',
badge: 'STEM',
},
{
id: 'o3-mini',
name: 'o3-mini',
provider: 'openai',
providerName: 'OpenAI',
description: 'High performance reasoning model',
badge: 'Reasoning',
description: 'Cost-sensitive GPT-5.6 tier for high-volume work.',
badge: 'Efficient',
},
// Anthropic
{
id: 'claude-3-5-sonnet',
name: 'Claude 3.5 Sonnet',
id: 'claude-opus-5',
name: 'Claude Opus 5',
provider: 'anthropic',
providerName: 'Anthropic',
description: 'State-of-the-art coding & software engineering intelligence',
badge: 'Top Code Quality',
description: 'Anthropic model for complex agentic coding and enterprise work.',
badge: 'Highest Capability',
isPopular: true,
},
{
id: 'claude-3-5-haiku',
name: 'Claude 3.5 Haiku',
id: 'claude-sonnet-5',
name: 'Claude Sonnet 5',
provider: 'anthropic',
providerName: 'Anthropic',
description: 'Ultra-fast lightweight coding assistant',
badge: 'Speed',
description: 'Current Sonnet model balancing speed and intelligence.',
badge: 'Balanced',
isPopular: true,
},
{
id: 'claude-haiku-4-5-20251001',
name: 'Claude Haiku 4.5',
provider: 'anthropic',
providerName: 'Anthropic',
description: 'Fast Claude model for lower-latency tasks.',
badge: 'Fast',
},
// Groq
{
id: 'llama-3.3-70b',
name: 'Llama 3.3 70B (Groq)',
id: 'openai/gpt-oss-120b',
name: 'GPT-OSS 120B (Groq)',
provider: 'groq',
providerName: 'Groq Cloud',
description: 'Ultra-fast LPU inference for open weights models',
badge: 'Open Weights',
description: 'Groq production-hosted 120B open-weight reasoning model.',
badge: 'Production',
isPopular: true,
},
{
id: 'deepseek-r1-distill',
name: 'DeepSeek R1 Distill (Groq)',
id: 'openai/gpt-oss-20b',
name: 'GPT-OSS 20B (Groq)',
provider: 'groq',
providerName: 'Groq Cloud',
description: 'Open reasoning model distilled for fast execution',
badge: 'Reasoning',
description: 'Groq production-hosted 20B model optimized for throughput.',
badge: 'Production',
},
];
@@ -165,9 +162,15 @@ export function getModelDescriptor(modelId: string): ModelDescriptor {
return {
id: modelId,
name: modelId,
provider: 'google',
providerName: 'Custom Provider',
description: 'Custom model execution engine',
badge: 'Custom',
provider: modelId.startsWith('claude-')
? 'anthropic'
: modelId.startsWith('gemini-')
? 'google'
: modelId.startsWith('gpt-')
? 'openai'
: 'groq',
providerName: 'Configured Provider',
description: 'Previously configured model id. Provider availability is checked live at execution.',
badge: 'Configured',
};
}

View File

@@ -849,7 +849,7 @@ CREATE TABLE orders (
export const DEFAULT_CREDENTIALS: LlmEngineConfig = {
email: '',
isVerified: true,
selectedModels: ['gemini-3.7-flash', 'gpt-4o'],
selectedModels: ['gemini-3.7-flash'],
mainModel: 'gemini-3.7-flash',
apiKeys: {
geminiApiKey: '',

View File

@@ -7,6 +7,7 @@ export async function executeChatGptAutomation(params: {
projectName: string;
sessionId: string;
driverOptions: any;
history?: { role: 'user' | 'assistant'; content: string }[];
}) {
const response = await fetch('/api/chatgpt/execute', {
method: 'POST',

View File

@@ -85,7 +85,7 @@ export interface ProviderApiKeys {
export interface LlmEngineConfig {
email?: string;
isVerified?: boolean;
selectedModels: string[]; // e.g. ['gemini-3.7-flash', 'gpt-4o']
selectedModels: string[]; // e.g. ['gemini-3.7-flash', 'gpt-5.6']
mainModel: string; // default primary model
apiKeys: ProviderApiKeys;
autoAttachArtifacts: boolean;
@@ -159,7 +159,7 @@ export interface SnapshotBinding {
createdByModel: string;
provider: LlmProvider;
isMainBranch: boolean;
branchName: string; // e.g. 'main', 'gemini-3.6-flash', 'gpt-4o'
branchName: string; // e.g. 'main', 'gemini-3.7-flash', 'gpt-5.6'
parentSnapshotId?: string;
boundArtifactVersions: Record<string, number>; // artifactId -> versionNumber
// Convenience helper properties