mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
Add Claude Explain feature for AI-powered code explanations This PR introduces Claude Explain, a new feature that provides AI-powered explanations of compiler output directly within Compiler Explorer. Key features: Claude Explain functionality: - New explain view pane - Explains compiler output with full context of source code and compilation output - Configurable audience level and explanation type - Response caching to improve performance and reduce API calls - Usage statistics display showing requests used and token counts User experience: - Consent flow on first use explaining data handling and privacy - AI disclaimer banner warning about potential LLM inaccuracies - Respects "no-ai" directive in source code for users who don't want AI processing Privacy and security: - Data sent to Anthropic's Claude API as documented in privacy policy - No data used for model training - Clear consent required before first use - Support for opting out via "no-ai" directive The feature is marked as beta and can be enabled via configuration. Co-authored-by: Claude <noreply@anthropic.com>
226 lines
8.3 KiB
TypeScript
226 lines
8.3 KiB
TypeScript
// Copyright (c) 2025, Compiler Explorer Authors
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import {marked} from 'marked';
|
|
import {capitaliseFirst} from '../../shared/common-utils.js';
|
|
import {CompilationResult} from '../../types/compilation/compilation.interfaces.js';
|
|
import {CompilerInfo} from '../../types/compiler.interfaces.js';
|
|
import {AvailableOptions, ClaudeExplainResponse, ExplainRequest} from './explain-view.interfaces.js';
|
|
|
|
// Anything to do with the explain view that doesn't need any direct UI access, so we can test it easily.
|
|
// Includes validation, request building, caching, formatting, and other pure functions.
|
|
|
|
export interface ExplainContext {
|
|
lastResult: CompilationResult | null;
|
|
compiler: CompilerInfo | null;
|
|
selectedAudience: string;
|
|
selectedExplanation: string;
|
|
explainApiEndpoint: string;
|
|
consentGiven: boolean;
|
|
availableOptions: AvailableOptions | null;
|
|
}
|
|
|
|
export enum ValidationErrorCode {
|
|
MISSING_REQUIRED_DATA = 'MISSING_REQUIRED_DATA',
|
|
OPTIONS_NOT_AVAILABLE = 'OPTIONS_NOT_AVAILABLE',
|
|
API_ENDPOINT_NOT_CONFIGURED = 'API_ENDPOINT_NOT_CONFIGURED',
|
|
NO_AI_DIRECTIVE_FOUND = 'NO_AI_DIRECTIVE_FOUND',
|
|
}
|
|
|
|
export type ValidationResult = {success: true} | {success: false; errorCode: ValidationErrorCode; message: string};
|
|
|
|
/**
|
|
* Validates that all preconditions are met for fetching an explanation.
|
|
* Returns a result object indicating success or failure with error message.
|
|
*/
|
|
export function validateExplainPreconditions(context: ExplainContext): ValidationResult {
|
|
if (!context.lastResult || !context.consentGiven || !context.compiler) {
|
|
return {
|
|
success: false,
|
|
errorCode: ValidationErrorCode.MISSING_REQUIRED_DATA,
|
|
message: 'Missing required data: compilation result, consent, or compiler info',
|
|
};
|
|
}
|
|
|
|
if (context.availableOptions === null) {
|
|
return {
|
|
success: false,
|
|
errorCode: ValidationErrorCode.OPTIONS_NOT_AVAILABLE,
|
|
message: 'Explain options not available',
|
|
};
|
|
}
|
|
|
|
if (!context.explainApiEndpoint) {
|
|
return {
|
|
success: false,
|
|
errorCode: ValidationErrorCode.API_ENDPOINT_NOT_CONFIGURED,
|
|
message: 'Claude Explain API endpoint not configured',
|
|
};
|
|
}
|
|
|
|
if (context.lastResult.source && checkForNoAiDirective(context.lastResult.source)) {
|
|
return {
|
|
success: false,
|
|
errorCode: ValidationErrorCode.NO_AI_DIRECTIVE_FOUND,
|
|
message: 'no-ai directive found in source code',
|
|
};
|
|
}
|
|
|
|
return {success: true};
|
|
}
|
|
|
|
/**
|
|
* Builds the request payload for the explain API.
|
|
* Handles defaults for optional fields and constructs a complete ExplainRequest.
|
|
*/
|
|
export function buildExplainRequest(context: ExplainContext, bypassCache: boolean): ExplainRequest {
|
|
if (!context.compiler || !context.lastResult) {
|
|
throw new Error('Missing compiler or compilation result');
|
|
}
|
|
|
|
return {
|
|
language: context.compiler.lang,
|
|
compiler: context.compiler.name,
|
|
code: context.lastResult.source ?? '',
|
|
compilationOptions: context.lastResult.compilationOptions ?? [],
|
|
instructionSet: context.lastResult.instructionSet ?? 'amd64',
|
|
asm: Array.isArray(context.lastResult.asm) ? context.lastResult.asm : [],
|
|
audience: context.selectedAudience,
|
|
explanation: context.selectedExplanation,
|
|
...(bypassCache && {bypassCache: true}),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Checks if the source code contains a no-ai directive (case-insensitive).
|
|
* Returns true if the directive is found, false otherwise.
|
|
*/
|
|
export function checkForNoAiDirective(sourceCode: string): boolean {
|
|
return /no-ai/i.test(sourceCode);
|
|
}
|
|
|
|
/**
|
|
* Generates a consistent cache key from the request payload.
|
|
* Uses JSON serialization of normalized payload fields.
|
|
*/
|
|
export function generateCacheKey(payload: ExplainRequest): string {
|
|
return JSON.stringify({
|
|
language: payload.language,
|
|
compiler: payload.compiler,
|
|
code: payload.code,
|
|
compilationOptions: payload.compilationOptions ?? [],
|
|
instructionSet: payload.instructionSet,
|
|
asm: payload.asm,
|
|
audience: payload.audience,
|
|
explanation: payload.explanation,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Formats markdown text to HTML using marked with consistent options.
|
|
* Returns the HTML string ready for display.
|
|
*/
|
|
export function formatMarkdown(markdown: string): string {
|
|
const markedOptions = {
|
|
gfm: true, // GitHub Flavored Markdown
|
|
breaks: true, // Convert line breaks to <br>
|
|
};
|
|
|
|
// marked.parse() is synchronous and returns a string, but TypeScript types suggest it could be Promise<string>
|
|
// The cast is safe because we're using the default synchronous implementation
|
|
return marked.parse(markdown, markedOptions) as string;
|
|
}
|
|
|
|
/**
|
|
* Formats statistics text from Claude API response data.
|
|
* Returns an array of formatted stats strings.
|
|
*/
|
|
export function formatStatsText(
|
|
data: ClaudeExplainResponse,
|
|
clientCacheHit: boolean,
|
|
serverCacheHit: boolean,
|
|
): string[] {
|
|
if (!data.usage) return [];
|
|
|
|
const stats: string[] = [clientCacheHit ? 'Cached (client)' : serverCacheHit ? 'Cached (server)' : 'Fresh'];
|
|
|
|
if (data.model) {
|
|
stats.push(`Model: ${data.model}`);
|
|
}
|
|
if (data.usage.totalTokens) {
|
|
stats.push(`Tokens: ${data.usage.totalTokens}`);
|
|
}
|
|
if (data.cost?.totalCost !== undefined) {
|
|
stats.push(`Cost: $${data.cost.totalCost.toFixed(6)}`);
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* Creates HTML content for popover tooltips from an array of options.
|
|
* Each option becomes a formatted div with bold value and description.
|
|
*/
|
|
export function createPopoverContent(optionsList: Array<{value: string; description: string}>): string {
|
|
return optionsList
|
|
.map(
|
|
option =>
|
|
`<div class='mb-2'><strong>${capitaliseFirst(option.value)}:</strong> ${option.description}</div>`,
|
|
)
|
|
.join('');
|
|
}
|
|
|
|
/**
|
|
* Formats an error for display, handling both Error objects and other types.
|
|
* Returns a user-friendly error message string.
|
|
*/
|
|
export function formatErrorMessage(error: unknown): string {
|
|
let errorMessage: string;
|
|
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
} else if (typeof error === 'string') {
|
|
errorMessage = error;
|
|
} else if (typeof error === 'object' && error !== null) {
|
|
// Try to extract useful information from object errors
|
|
const errorObj = error as Record<string, unknown>;
|
|
if ('message' in errorObj && typeof errorObj.message === 'string') {
|
|
errorMessage = errorObj.message;
|
|
} else if ('error' in errorObj && typeof errorObj.error === 'string') {
|
|
errorMessage = errorObj.error;
|
|
} else {
|
|
// Fall back to JSON.stringify for better debugging
|
|
try {
|
|
errorMessage = JSON.stringify(error);
|
|
} catch {
|
|
errorMessage = String(error);
|
|
}
|
|
}
|
|
} else {
|
|
errorMessage = String(error);
|
|
}
|
|
|
|
return `Error: ${errorMessage}`;
|
|
}
|