Backend: - Add POST /generate-extractor for AI code generation via Claude CLI - Add POST /generate-extractor/stream for SSE streaming generation - Add POST /validate-extractor with enhanced syntax checking - Add POST /check-dependencies for import analysis - Add POST /test-extractor for live OP2 file testing - Add ClaudeCodeSession service for managing CLI sessions Frontend: - Add lib/api/claude.ts with typed API functions - Enhance CodeEditorPanel with: - Streaming generation with live preview - Code snippets library (6 templates: displacement, stress, frequency, mass, energy, reaction) - Test button for live OP2 validation - Cancel button for stopping generation - Dependency warnings display - Integrate streaming and testing into NodeConfigPanelV2 Uses Claude CLI (--print mode) to leverage Pro/Max subscription without API costs.
349 lines
9.1 KiB
TypeScript
349 lines
9.1 KiB
TypeScript
/**
|
|
* Claude Code API Functions
|
|
*
|
|
* Provides typed API functions for interacting with Claude Code CLI
|
|
* through the backend endpoints.
|
|
*/
|
|
|
|
const API_BASE = '/api/claude-code';
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
export interface ExtractorGenerationRequest {
|
|
/** Description of what the extractor should do */
|
|
prompt: string;
|
|
/** Optional study ID for context */
|
|
study_id?: string;
|
|
/** Existing code to improve/modify */
|
|
existing_code?: string;
|
|
/** Expected output variable names */
|
|
output_names?: string[];
|
|
}
|
|
|
|
export interface ExtractorGenerationResponse {
|
|
/** Generated Python code */
|
|
code: string;
|
|
/** Detected output variable names */
|
|
outputs: string[];
|
|
/** Optional brief explanation */
|
|
explanation?: string;
|
|
}
|
|
|
|
export interface CodeValidationRequest {
|
|
/** Python code to validate */
|
|
code: string;
|
|
}
|
|
|
|
export interface CodeValidationResponse {
|
|
/** Whether the code is valid */
|
|
valid: boolean;
|
|
/** Error message if invalid */
|
|
error?: string;
|
|
}
|
|
|
|
export interface DependencyCheckResponse {
|
|
/** All imports found in code */
|
|
imports: string[];
|
|
/** Imports that are available */
|
|
available: string[];
|
|
/** Imports that are missing */
|
|
missing: string[];
|
|
/** Warnings about potentially problematic imports */
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface TestExtractorRequest {
|
|
/** Python code to test */
|
|
code: string;
|
|
/** Optional study ID for finding OP2 files */
|
|
study_id?: string;
|
|
/** Subcase ID to test against (default 1) */
|
|
subcase_id?: number;
|
|
}
|
|
|
|
export interface TestExtractorResponse {
|
|
/** Whether the test succeeded */
|
|
success: boolean;
|
|
/** Extracted output values */
|
|
outputs?: Record<string, number>;
|
|
/** Error message if failed */
|
|
error?: string;
|
|
/** Execution time in milliseconds */
|
|
execution_time_ms?: number;
|
|
}
|
|
|
|
// ============================================================================
|
|
// API Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Generate Python extractor code using Claude Code CLI.
|
|
*
|
|
* @param request - Generation request with prompt and context
|
|
* @returns Promise with generated code and detected outputs
|
|
* @throws Error if generation fails
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const result = await generateExtractorCode({
|
|
* prompt: "Extract maximum von Mises stress from solid elements",
|
|
* output_names: ["max_stress"],
|
|
* });
|
|
* console.log(result.code);
|
|
* ```
|
|
*/
|
|
export async function generateExtractorCode(
|
|
request: ExtractorGenerationRequest
|
|
): Promise<ExtractorGenerationResponse> {
|
|
const response = await fetch(`${API_BASE}/generate-extractor`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(request),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: 'Generation failed' }));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Validate Python extractor code syntax.
|
|
*
|
|
* @param code - Python code to validate
|
|
* @returns Promise with validation result
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const result = await validateExtractorCode("def extract(): pass");
|
|
* if (!result.valid) {
|
|
* console.error(result.error);
|
|
* }
|
|
* ```
|
|
*/
|
|
export async function validateExtractorCode(
|
|
code: string
|
|
): Promise<CodeValidationResponse> {
|
|
const response = await fetch(`${API_BASE}/validate-extractor`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ code }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
// Even if HTTP fails, return as invalid
|
|
return {
|
|
valid: false,
|
|
error: `Validation request failed: HTTP ${response.status}`,
|
|
};
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Check dependencies in Python code.
|
|
*
|
|
* @param code - Python code to analyze
|
|
* @returns Promise with dependency check results
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const result = await checkCodeDependencies("import numpy as np");
|
|
* if (result.missing.length > 0) {
|
|
* console.warn("Missing:", result.missing);
|
|
* }
|
|
* ```
|
|
*/
|
|
export async function checkCodeDependencies(
|
|
code: string
|
|
): Promise<DependencyCheckResponse> {
|
|
const response = await fetch(`${API_BASE}/check-dependencies`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ code }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
return {
|
|
imports: [],
|
|
available: [],
|
|
missing: [],
|
|
warnings: ['Dependency check failed'],
|
|
};
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Test extractor code against a sample OP2 file.
|
|
*
|
|
* @param request - Test request with code and optional study context
|
|
* @returns Promise with test results
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const result = await testExtractorCode({
|
|
* code: "def extract(...): ...",
|
|
* study_id: "bracket_v1",
|
|
* });
|
|
* if (result.success) {
|
|
* console.log("Outputs:", result.outputs);
|
|
* }
|
|
* ```
|
|
*/
|
|
export async function testExtractorCode(
|
|
request: TestExtractorRequest
|
|
): Promise<TestExtractorResponse> {
|
|
const response = await fetch(`${API_BASE}/test-extractor`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(request),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
return {
|
|
success: false,
|
|
error: `Test request failed: HTTP ${response.status}`,
|
|
};
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Check if Claude Code CLI is available.
|
|
*
|
|
* @returns Promise with availability status
|
|
*/
|
|
export async function checkClaudeStatus(): Promise<{
|
|
available: boolean;
|
|
message: string;
|
|
}> {
|
|
try {
|
|
const response = await fetch('/api/claude/status');
|
|
if (!response.ok) {
|
|
return { available: false, message: 'Status check failed' };
|
|
}
|
|
return response.json();
|
|
} catch {
|
|
return { available: false, message: 'Cannot reach backend' };
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Streaming Generation
|
|
// ============================================================================
|
|
|
|
export interface StreamingGenerationCallbacks {
|
|
/** Called when a new token is received */
|
|
onToken?: (token: string) => void;
|
|
/** Called when generation is complete */
|
|
onComplete?: (code: string, outputs: string[]) => void;
|
|
/** Called when an error occurs */
|
|
onError?: (error: string) => void;
|
|
}
|
|
|
|
/**
|
|
* Stream Python extractor code generation using Server-Sent Events.
|
|
*
|
|
* This provides real-time feedback as Claude generates the code,
|
|
* showing tokens as they arrive.
|
|
*
|
|
* @param request - Generation request with prompt and context
|
|
* @param callbacks - Callbacks for streaming events
|
|
* @returns AbortController to cancel the stream
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const controller = streamExtractorCode(
|
|
* { prompt: "Extract maximum stress" },
|
|
* {
|
|
* onToken: (token) => setPartialCode(prev => prev + token),
|
|
* onComplete: (code, outputs) => {
|
|
* setCode(code);
|
|
* setIsGenerating(false);
|
|
* },
|
|
* onError: (error) => setError(error),
|
|
* }
|
|
* );
|
|
*
|
|
* // To cancel:
|
|
* controller.abort();
|
|
* ```
|
|
*/
|
|
export function streamExtractorCode(
|
|
request: ExtractorGenerationRequest,
|
|
callbacks: StreamingGenerationCallbacks
|
|
): AbortController {
|
|
const controller = new AbortController();
|
|
|
|
(async () => {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/generate-extractor/stream`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(request),
|
|
signal: controller.signal,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: 'Stream failed' }));
|
|
callbacks.onError?.(error.detail || `HTTP ${response.status}`);
|
|
return;
|
|
}
|
|
|
|
const reader = response.body?.getReader();
|
|
if (!reader) {
|
|
callbacks.onError?.('No response body');
|
|
return;
|
|
}
|
|
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
// Parse SSE events from buffer
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith('data: ')) {
|
|
try {
|
|
const data = JSON.parse(line.slice(6));
|
|
|
|
if (data.type === 'token') {
|
|
callbacks.onToken?.(data.content);
|
|
} else if (data.type === 'done') {
|
|
callbacks.onComplete?.(data.code, data.outputs);
|
|
} else if (data.type === 'error') {
|
|
callbacks.onError?.(data.message);
|
|
}
|
|
} catch {
|
|
// Ignore parse errors for incomplete JSON
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
// User cancelled, don't report as error
|
|
return;
|
|
}
|
|
callbacks.onError?.(error instanceof Error ? error.message : 'Stream failed');
|
|
}
|
|
})();
|
|
|
|
return controller;
|
|
}
|