Path: blob/main/extensions/copilot/src/extension/prompts/node/panel/image.tsx
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { RequestType } from '@vscode/copilot-api';6import * as l10n from '@vscode/l10n';7import { Image as BaseImage, BasePromptElementProps, ChatResponseReferencePartStatusKind, PromptElement, PromptReference, PromptSizing, UserMessage } from '@vscode/prompt-tsx';8import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';9import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';10import { modelCanUseImageURL } from '../../../../platform/endpoint/common/chatModelCapabilities';11import { IImageService } from '../../../../platform/image/common/imageService';12import { ILogService } from '../../../../platform/log/common/logService';13import { IExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';14import { getMimeType } from '../../../../util/common/imageUtils';15import { Uri } from '../../../../vscodeTypes';16import { IPromptEndpoint } from '../base/promptRenderer';1718export interface ImageProps extends BasePromptElementProps {19variableName: string;20variableValue: Uint8Array | Promise<Uint8Array>;21omitReferences?: boolean;22reference?: Uri;23}2425/**26* Props for rendering an image that was previously rendered and stored in conversation history.27* These images are already processed (base64 or URL) and don't need re-uploading.28*/29export interface HistoricalImageProps extends BasePromptElementProps {30/** The image source - either a base64 string or URL */31src: string;32/** The detail level for the image */33detail?: 'auto' | 'low' | 'high';34/** The MIME type of the image */35mimeType?: string;36}3738/**39* Renders an image from conversation history.40* Checks if the current model supports vision and omits the image if not.41*/42export class HistoricalImage extends PromptElement<HistoricalImageProps, unknown> {43constructor(44props: HistoricalImageProps,45@IPromptEndpoint private readonly promptEndpoint: IPromptEndpoint,46@IAuthenticationService private readonly authService: IAuthenticationService,47) {48super(props);49}5051override async render(_state: unknown, sizing: PromptSizing) {52// If the model doesn't support vision, omit historical images53if (!this.promptEndpoint.supportsVision || !this.authService.copilotToken?.isEditorPreviewFeaturesEnabled()) {54return undefined;55}5657return <BaseImage src={this.props.src} detail={this.props.detail} mimeType={this.props.mimeType} />;58}59}6061export class Image extends PromptElement<ImageProps, unknown> {62constructor(63props: ImageProps,64@IPromptEndpoint private readonly promptEndpoint: IPromptEndpoint,65@IAuthenticationService private readonly authService: IAuthenticationService,66@ILogService private readonly logService: ILogService,67@IImageService private readonly imageService: IImageService,68@IConfigurationService private readonly configurationService: IConfigurationService,69@IExperimentationService private readonly experimentationService: IExperimentationService70) {71super(props);72}7374override async render(_state: unknown, sizing: PromptSizing) {75const options = { status: { description: l10n.t("{0} does not support images.", this.promptEndpoint.model), kind: ChatResponseReferencePartStatusKind.Omitted } };7677const fillerUri: Uri = this.props.reference ?? Uri.parse('Attached Image');7879try {80if (!this.promptEndpoint.supportsVision || !this.authService.copilotToken?.isEditorPreviewFeaturesEnabled()) {81if (this.props.omitReferences) {82return;83}8485return (86<>87<references value={[new PromptReference(this.props.variableName ? { variableName: this.props.variableName, value: fillerUri } : fillerUri, undefined, options)]} />88</>89);90}91const variable = await this.props.variableValue;92let imageSource = Buffer.from(variable).toString('base64');93let imageMimeType: string | undefined = undefined;9495const isChatRequest = typeof this.promptEndpoint.urlOrRequestMetadata !== 'string' && (this.promptEndpoint.urlOrRequestMetadata.type === RequestType.ChatCompletions || this.promptEndpoint.urlOrRequestMetadata.type === RequestType.ChatResponses || this.promptEndpoint.urlOrRequestMetadata.type === RequestType.ChatMessages);96const enabled = this.configurationService.getExperimentBasedConfig(ConfigKey.EnableChatImageUpload, this.experimentationService);97if (isChatRequest && enabled && modelCanUseImageURL(this.promptEndpoint)) {98try {99const githubToken = (await this.authService.getGitHubSession('any', { silent: true }))?.accessToken;100const mimeType = getMimeType(imageSource) ?? imageMimeType;101const uri = await this.imageService.uploadChatImageAttachment(variable, this.props.variableName, mimeType, githubToken);102if (uri) {103imageSource = uri.toString();104imageMimeType = mimeType;105}106} catch (error) {107this.logService.warn(`Image upload failed, using base64 fallback: ${error}`);108}109}110111return (112<UserMessage priority={0}>113<BaseImage src={imageSource} detail='high' mimeType={imageMimeType} />114{this.props.reference && (115<references value={[new PromptReference(this.props.variableName ? { variableName: this.props.variableName, value: fillerUri } : fillerUri, undefined)]} />116)}117</UserMessage>118);119} catch (err) {120if (this.props.omitReferences) {121return;122}123124return (125<>126<references value={[new PromptReference(this.props.variableName ? { variableName: this.props.variableName, value: fillerUri } : fillerUri, undefined, options)]} />127</>);128}129}130}131132133