Files
compiler-explorer/static/panes/explain-view-utils.ts
Matt Godbolt c29ad46f3a [Not Live; disabled by default] Add Claude Explain feature for AI-powered assembly explanations (#7749)
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>
2025-08-05 09:31:48 -05:00

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}`;
}