Merge branch 'main' into system-prompt-variations

This commit is contained in:
Anirban Kar
2024-12-15 21:01:52 +05:30
committed by GitHub
22 changed files with 644 additions and 113 deletions

View File

@@ -1 +1 @@
{ "commit": "2b8236f9880984508f2ae17575b587bb9d938e55" }
{ "commit": "9cd9ee9088467882e1e4efdf491959619307cc9d" }

View File

@@ -26,6 +26,8 @@ import FilePreview from './FilePreview';
import { ModelSelector } from '~/components/chat/ModelSelector';
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
import type { IProviderSetting, ProviderInfo } from '~/types/model';
import { ScreenshotStateManager } from './ScreenshotStateManager';
import { toast } from 'react-toastify';
const TEXTAREA_MIN_HEIGHT = 76;
@@ -376,6 +378,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
setImageDataList?.(imageDataList.filter((_, i) => i !== index));
}}
/>
<ClientOnly>
{() => (
<ScreenshotStateManager
setUploadedFiles={setUploadedFiles}
setImageDataList={setImageDataList}
uploadedFiles={uploadedFiles}
imageDataList={imageDataList}
/>
)}
</ClientOnly>
<div
className={classNames(
'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
@@ -431,6 +443,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
return;
}
// ignore if using input method engine
if (event.nativeEvent.isComposing) {
return;
}
handleSendMessage?.(event);
}
}}
@@ -476,22 +493,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
className={classNames(
'transition-all',
enhancingPrompt ? 'opacity-100' : '',
promptEnhanced ? 'text-bolt-elements-item-contentAccent' : '',
promptEnhanced ? 'pr-1.5' : '',
promptEnhanced ? 'enabled:hover:bg-bolt-elements-item-backgroundAccent' : '',
)}
onClick={() => enhancePrompt?.()}
onClick={() => {
enhancePrompt?.();
toast.success('Prompt enhanced!');
}}
>
{enhancingPrompt ? (
<>
<div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
<div className="ml-1.5">Enhancing prompt...</div>
</>
<div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
) : (
<>
<div className="i-bolt:stars text-xl"></div>
{promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
</>
<div className="i-bolt:stars text-xl"></div>
)}
</IconButton>

View File

@@ -0,0 +1,33 @@
import { useEffect } from 'react';
interface ScreenshotStateManagerProps {
setUploadedFiles?: (files: File[]) => void;
setImageDataList?: (dataList: string[]) => void;
uploadedFiles: File[];
imageDataList: string[];
}
export const ScreenshotStateManager = ({
setUploadedFiles,
setImageDataList,
uploadedFiles,
imageDataList,
}: ScreenshotStateManagerProps) => {
useEffect(() => {
if (setUploadedFiles && setImageDataList) {
(window as any).__BOLT_SET_UPLOADED_FILES__ = setUploadedFiles;
(window as any).__BOLT_SET_IMAGE_DATA_LIST__ = setImageDataList;
(window as any).__BOLT_UPLOADED_FILES__ = uploadedFiles;
(window as any).__BOLT_IMAGE_DATA_LIST__ = imageDataList;
}
return () => {
delete (window as any).__BOLT_SET_UPLOADED_FILES__;
delete (window as any).__BOLT_SET_IMAGE_DATA_LIST__;
delete (window as any).__BOLT_UPLOADED_FILES__;
delete (window as any).__BOLT_IMAGE_DATA_LIST__;
};
}, [setUploadedFiles, setImageDataList, uploadedFiles, imageDataList]);
return null;
};

View File

@@ -27,8 +27,8 @@ export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
{ id: 'chat-history', label: 'Chat History', icon: 'i-ph:book', component: <ChatHistoryTab /> },
{ id: 'providers', label: 'Providers', icon: 'i-ph:key', component: <ProvidersTab /> },
{ id: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
{ id: 'connection', label: 'Connection', icon: 'i-ph:link', component: <ConnectionsTab /> },
{ id: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
...(debug
? [
{

View File

@@ -22,17 +22,20 @@ export default function ChatHistoryTab() {
};
const handleDeleteAllChats = async () => {
const confirmDelete = window.confirm("Are you sure you want to delete all chats? This action cannot be undone.");
if (!confirmDelete) {
return; // Exit if the user cancels
}
if (!db) {
const error = new Error('Database is not available');
logStore.logError('Failed to delete chats - DB unavailable', error);
toast.error('Database is not available');
return;
}
try {
setIsDeleting(true);
const allChats = await getAll(db);
await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
logStore.logSystem('All chats deleted successfully', { count: allChats.length });
@@ -52,7 +55,6 @@ export default function ChatHistoryTab() {
const error = new Error('Database is not available');
logStore.logError('Failed to export chats - DB unavailable', error);
toast.error('Database is not available');
return;
}

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useSettings } from '~/lib/hooks/useSettings';
import commit from '~/commit.json';
import { toast } from 'react-toastify';
interface ProviderStatus {
name: string;
@@ -308,8 +309,9 @@ export default function DebugTab() {
Version: versionHash,
Timestamp: new Date().toISOString(),
};
navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => {
alert('Debug information copied to clipboard!');
toast.success('Debug information copied to clipboard!');
});
}, [activeProviders, systemInfo]);

View File

@@ -4,19 +4,21 @@ import { PromptLibrary } from '~/lib/common/prompt-library';
import { useSettings } from '~/lib/hooks/useSettings';
export default function FeaturesTab() {
const { debug, enableDebugMode, isLocalModel, enableLocalModels, eventLogs, enableEventLogs, promptId, setPromptId } =
useSettings();
const handleToggle = (enabled: boolean) => {
enableDebugMode(enabled);
enableEventLogs(enabled);
};
return (
<div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
<div className="mb-6">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Optional Features</h3>
<div className="flex items-center justify-between mb-6">
<span className="text-bolt-elements-textPrimary">Debug Info</span>
<Switch className="ml-auto" checked={debug} onCheckedChange={enableDebugMode} />
</div>
<div className="flex items-center justify-between mb-6">
<span className="text-bolt-elements-textPrimary">Event Logs</span>
<Switch className="ml-auto" checked={eventLogs} onCheckedChange={enableEventLogs} />
<div className="flex items-center justify-between mb-2">
<span className="text-bolt-elements-textPrimary">Debug Features</span>
<Switch className="ml-auto" checked={debug} onCheckedChange={handleToggle} />
</div>
</div>
@@ -25,8 +27,9 @@ export default function FeaturesTab() {
<p className="text-sm text-bolt-elements-textSecondary mb-4">
Disclaimer: Experimental features may be unstable and are subject to change.
</p>
<div className="flex items-center justify-between pt-4 mb-4">
<span className="text-bolt-elements-textPrimary">Enable Local Models</span>
<div className="flex items-center justify-between mb-2">
<span className="text-bolt-elements-textPrimary">Experimental Providers</span>
<Switch className="ml-auto" checked={isLocalModel} onCheckedChange={enableLocalModels} />
</div>
<div className="flex items-start justify-between pt-4 mb-2 gap-2">

View File

@@ -5,6 +5,9 @@ import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settin
import type { IProviderConfig } from '~/types/model';
import { logStore } from '~/lib/stores/logs';
// Import a default fallback icon
import DefaultIcon from '/icons/Ollama.svg'; // Adjust the path as necessary
export default function ProvidersTab() {
const { providers, updateProviderSettings, isLocalModel } = useSettings();
const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
@@ -51,7 +54,14 @@ export default function ProvidersTab() {
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<img src={`/icons/${provider.name}.svg`} alt={`${provider.name} icon`} className="w-6 h-6 dark:invert" />
<img
src={`/icons/${provider.name}.svg`} // Attempt to load the specific icon
onError={(e) => { // Fallback to default icon on error
e.currentTarget.src = DefaultIcon;
}}
alt={`${provider.name} icon`}
className="w-6 h-6 dark:invert"
/>
<span className="text-bolt-elements-textPrimary">{provider.name}</span>
</div>
<Switch

View File

@@ -2,6 +2,7 @@ import { memo, useEffect, useMemo, useState, type ReactNode } from 'react';
import type { FileMap } from '~/lib/stores/files';
import { classNames } from '~/utils/classNames';
import { createScopedLogger, renderLogger } from '~/utils/logger';
import * as ContextMenu from '@radix-ui/react-context-menu';
const logger = createScopedLogger('FileTree');
@@ -110,6 +111,22 @@ export const FileTree = memo(
});
};
const onCopyPath = (fileOrFolder: FileNode | FolderNode) => {
try {
navigator.clipboard.writeText(fileOrFolder.fullPath);
} catch (error) {
logger.error(error);
}
};
const onCopyRelativePath = (fileOrFolder: FileNode | FolderNode) => {
try {
navigator.clipboard.writeText(fileOrFolder.fullPath.substring((rootFolder || '').length));
} catch (error) {
logger.error(error);
}
};
return (
<div className={classNames('text-sm', className, 'overflow-y-auto')}>
{filteredFileList.map((fileOrFolder) => {
@@ -121,6 +138,12 @@ export const FileTree = memo(
selected={selectedFile === fileOrFolder.fullPath}
file={fileOrFolder}
unsavedChanges={unsavedFiles?.has(fileOrFolder.fullPath)}
onCopyPath={() => {
onCopyPath(fileOrFolder);
}}
onCopyRelativePath={() => {
onCopyRelativePath(fileOrFolder);
}}
onClick={() => {
onFileSelect?.(fileOrFolder.fullPath);
}}
@@ -134,6 +157,12 @@ export const FileTree = memo(
folder={fileOrFolder}
selected={allowFolderSelection && selectedFile === fileOrFolder.fullPath}
collapsed={collapsedFolders.has(fileOrFolder.fullPath)}
onCopyPath={() => {
onCopyPath(fileOrFolder);
}}
onCopyRelativePath={() => {
onCopyRelativePath(fileOrFolder);
}}
onClick={() => {
toggleCollapseState(fileOrFolder.fullPath);
}}
@@ -156,26 +185,67 @@ interface FolderProps {
folder: FolderNode;
collapsed: boolean;
selected?: boolean;
onCopyPath: () => void;
onCopyRelativePath: () => void;
onClick: () => void;
}
function Folder({ folder: { depth, name }, collapsed, selected = false, onClick }: FolderProps) {
interface FolderContextMenuProps {
onCopyPath?: () => void;
onCopyRelativePath?: () => void;
children: ReactNode;
}
function ContextMenuItem({ onSelect, children }: { onSelect?: () => void; children: ReactNode }) {
return (
<NodeButton
className={classNames('group', {
'bg-transparent text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive':
!selected,
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
})}
depth={depth}
iconClasses={classNames({
'i-ph:caret-right scale-98': collapsed,
'i-ph:caret-down scale-98': !collapsed,
})}
onClick={onClick}
<ContextMenu.Item
onSelect={onSelect}
className="flex items-center gap-2 px-2 py-1.5 outline-0 text-sm text-bolt-elements-textPrimary cursor-pointer ws-nowrap text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive rounded-md"
>
{name}
</NodeButton>
<span className="size-4 shrink-0"></span>
<span>{children}</span>
</ContextMenu.Item>
);
}
function FileContextMenu({ onCopyPath, onCopyRelativePath, children }: FolderContextMenuProps) {
return (
<ContextMenu.Root>
<ContextMenu.Trigger>{children}</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content
style={{ zIndex: 998 }}
className="border border-bolt-elements-borderColor rounded-md z-context-menu bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-2 data-[state=open]:animate-in animate-duration-100 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-98 w-56"
>
<ContextMenu.Group className="p-1 border-b-px border-solid border-bolt-elements-borderColor">
<ContextMenuItem onSelect={onCopyPath}>Copy path</ContextMenuItem>
<ContextMenuItem onSelect={onCopyRelativePath}>Copy relative path</ContextMenuItem>
</ContextMenu.Group>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Root>
);
}
function Folder({ folder, collapsed, selected = false, onCopyPath, onCopyRelativePath, onClick }: FolderProps) {
return (
<FileContextMenu onCopyPath={onCopyPath} onCopyRelativePath={onCopyRelativePath}>
<NodeButton
className={classNames('group', {
'bg-transparent text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive':
!selected,
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
})}
depth={folder.depth}
iconClasses={classNames({
'i-ph:caret-right scale-98': collapsed,
'i-ph:caret-down scale-98': !collapsed,
})}
onClick={onClick}
>
{folder.name}
</NodeButton>
</FileContextMenu>
);
}
@@ -183,31 +253,43 @@ interface FileProps {
file: FileNode;
selected: boolean;
unsavedChanges?: boolean;
onCopyPath: () => void;
onCopyRelativePath: () => void;
onClick: () => void;
}
function File({ file: { depth, name }, onClick, selected, unsavedChanges = false }: FileProps) {
function File({
file: { depth, name },
onClick,
onCopyPath,
onCopyRelativePath,
selected,
unsavedChanges = false,
}: FileProps) {
return (
<NodeButton
className={classNames('group', {
'bg-transparent hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-item-contentDefault': !selected,
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
})}
depth={depth}
iconClasses={classNames('i-ph:file-duotone scale-98', {
'group-hover:text-bolt-elements-item-contentActive': !selected,
})}
onClick={onClick}
>
<div
className={classNames('flex items-center', {
<FileContextMenu onCopyPath={onCopyPath} onCopyRelativePath={onCopyRelativePath}>
<NodeButton
className={classNames('group', {
'bg-transparent hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-item-contentDefault':
!selected,
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
})}
depth={depth}
iconClasses={classNames('i-ph:file-duotone scale-98', {
'group-hover:text-bolt-elements-item-contentActive': !selected,
})}
onClick={onClick}
>
<div className="flex-1 truncate pr-2">{name}</div>
{unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />}
</div>
</NodeButton>
<div
className={classNames('flex items-center', {
'group-hover:text-bolt-elements-item-contentActive': !selected,
})}
>
<div className="flex-1 truncate pr-2">{name}</div>
{unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />}
</div>
</NodeButton>
</FileContextMenu>
);
}

View File

@@ -3,6 +3,7 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { IconButton } from '~/components/ui/IconButton';
import { workbenchStore } from '~/lib/stores/workbench';
import { PortDropdown } from './PortDropdown';
import { ScreenshotSelector } from './ScreenshotSelector';
type ResizeSide = 'left' | 'right' | null;
@@ -20,6 +21,7 @@ export const Preview = memo(() => {
const [url, setUrl] = useState('');
const [iframeUrl, setIframeUrl] = useState<string | undefined>();
const [isSelectionMode, setIsSelectionMode] = useState(false);
// Toggle between responsive mode and device mode
const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
@@ -218,12 +220,17 @@ export const Preview = memo(() => {
)}
<div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
<IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
<IconButton
icon="i-ph:selection"
onClick={() => setIsSelectionMode(!isSelectionMode)}
className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
/>
<div
className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive
focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
>
<input
title="URL"
ref={inputRef}
className="w-full bg-transparent outline-none"
type="text"
@@ -281,7 +288,20 @@ export const Preview = memo(() => {
}}
>
{activePreview ? (
<iframe ref={iframeRef} className="border-none w-full h-full bg-white" src={iframeUrl} allowFullScreen />
<>
<iframe
ref={iframeRef}
title="preview"
className="border-none w-full h-full bg-white"
src={iframeUrl}
allowFullScreen
/>
<ScreenshotSelector
isSelectionMode={isSelectionMode}
setIsSelectionMode={setIsSelectionMode}
containerRef={iframeRef}
/>
</>
) : (
<div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
)}

View File

@@ -0,0 +1,293 @@
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
interface ScreenshotSelectorProps {
isSelectionMode: boolean;
setIsSelectionMode: (mode: boolean) => void;
containerRef: React.RefObject<HTMLElement>;
}
export const ScreenshotSelector = memo(
({ isSelectionMode, setIsSelectionMode, containerRef }: ScreenshotSelectorProps) => {
const [isCapturing, setIsCapturing] = useState(false);
const [selectionStart, setSelectionStart] = useState<{ x: number; y: number } | null>(null);
const [selectionEnd, setSelectionEnd] = useState<{ x: number; y: number } | null>(null);
const mediaStreamRef = useRef<MediaStream | null>(null);
const videoRef = useRef<HTMLVideoElement | null>(null);
useEffect(() => {
// Cleanup function to stop all tracks when component unmounts
return () => {
if (videoRef.current) {
videoRef.current.pause();
videoRef.current.srcObject = null;
videoRef.current.remove();
videoRef.current = null;
}
if (mediaStreamRef.current) {
mediaStreamRef.current.getTracks().forEach((track) => track.stop());
mediaStreamRef.current = null;
}
};
}, []);
const initializeStream = async () => {
if (!mediaStreamRef.current) {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
audio: false,
video: {
displaySurface: 'window',
preferCurrentTab: true,
surfaceSwitching: 'include',
systemAudio: 'exclude',
},
} as MediaStreamConstraints);
// Add handler for when sharing stops
stream.addEventListener('inactive', () => {
if (videoRef.current) {
videoRef.current.pause();
videoRef.current.srcObject = null;
videoRef.current.remove();
videoRef.current = null;
}
if (mediaStreamRef.current) {
mediaStreamRef.current.getTracks().forEach((track) => track.stop());
mediaStreamRef.current = null;
}
setIsSelectionMode(false);
setSelectionStart(null);
setSelectionEnd(null);
setIsCapturing(false);
});
mediaStreamRef.current = stream;
// Initialize video element if needed
if (!videoRef.current) {
const video = document.createElement('video');
video.style.opacity = '0';
video.style.position = 'fixed';
video.style.pointerEvents = 'none';
video.style.zIndex = '-1';
document.body.appendChild(video);
videoRef.current = video;
}
// Set up video with the stream
videoRef.current.srcObject = stream;
await videoRef.current.play();
} catch (error) {
console.error('Failed to initialize stream:', error);
setIsSelectionMode(false);
toast.error('Failed to initialize screen capture');
}
}
return mediaStreamRef.current;
};
const handleCopySelection = useCallback(async () => {
if (!isSelectionMode || !selectionStart || !selectionEnd || !containerRef.current) {
return;
}
setIsCapturing(true);
try {
const stream = await initializeStream();
if (!stream || !videoRef.current) {
return;
}
// Wait for video to be ready
await new Promise((resolve) => setTimeout(resolve, 300));
// Create temporary canvas for full screenshot
const tempCanvas = document.createElement('canvas');
tempCanvas.width = videoRef.current.videoWidth;
tempCanvas.height = videoRef.current.videoHeight;
const tempCtx = tempCanvas.getContext('2d');
if (!tempCtx) {
throw new Error('Failed to get temporary canvas context');
}
// Draw the full video frame
tempCtx.drawImage(videoRef.current, 0, 0);
// Calculate scale factor between video and screen
const scaleX = videoRef.current.videoWidth / window.innerWidth;
const scaleY = videoRef.current.videoHeight / window.innerHeight;
// Get window scroll position
const scrollX = window.scrollX;
const scrollY = window.scrollY + 40;
// Get the container's position in the page
const containerRect = containerRef.current.getBoundingClientRect();
// Offset adjustments for more accurate clipping
const leftOffset = -9; // Adjust left position
const bottomOffset = -14; // Adjust bottom position
// Calculate the scaled coordinates with scroll offset and adjustments
const scaledX = Math.round(
(containerRect.left + Math.min(selectionStart.x, selectionEnd.x) + scrollX + leftOffset) * scaleX,
);
const scaledY = Math.round(
(containerRect.top + Math.min(selectionStart.y, selectionEnd.y) + scrollY + bottomOffset) * scaleY,
);
const scaledWidth = Math.round(Math.abs(selectionEnd.x - selectionStart.x) * scaleX);
const scaledHeight = Math.round(Math.abs(selectionEnd.y - selectionStart.y) * scaleY);
// Create final canvas for the cropped area
const canvas = document.createElement('canvas');
canvas.width = Math.round(Math.abs(selectionEnd.x - selectionStart.x));
canvas.height = Math.round(Math.abs(selectionEnd.y - selectionStart.y));
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get canvas context');
}
// Draw the cropped area
ctx.drawImage(tempCanvas, scaledX, scaledY, scaledWidth, scaledHeight, 0, 0, canvas.width, canvas.height);
// Convert to blob
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('Failed to create blob'));
}
}, 'image/png');
});
// Create a FileReader to convert blob to base64
const reader = new FileReader();
reader.onload = (e) => {
const base64Image = e.target?.result as string;
// Find the textarea element
const textarea = document.querySelector('textarea');
if (textarea) {
// Get the setters from the BaseChat component
const setUploadedFiles = (window as any).__BOLT_SET_UPLOADED_FILES__;
const setImageDataList = (window as any).__BOLT_SET_IMAGE_DATA_LIST__;
const uploadedFiles = (window as any).__BOLT_UPLOADED_FILES__ || [];
const imageDataList = (window as any).__BOLT_IMAGE_DATA_LIST__ || [];
if (setUploadedFiles && setImageDataList) {
// Update the files and image data
const file = new File([blob], 'screenshot.png', { type: 'image/png' });
setUploadedFiles([...uploadedFiles, file]);
setImageDataList([...imageDataList, base64Image]);
toast.success('Screenshot captured and added to chat');
} else {
toast.error('Could not add screenshot to chat');
}
}
};
reader.readAsDataURL(blob);
} catch (error) {
console.error('Failed to capture screenshot:', error);
toast.error('Failed to capture screenshot');
if (mediaStreamRef.current) {
mediaStreamRef.current.getTracks().forEach((track) => track.stop());
mediaStreamRef.current = null;
}
} finally {
setIsCapturing(false);
setSelectionStart(null);
setSelectionEnd(null);
setIsSelectionMode(false); // Turn off selection mode after capture
}
}, [isSelectionMode, selectionStart, selectionEnd, containerRef, setIsSelectionMode]);
const handleSelectionStart = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isSelectionMode) {
return;
}
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setSelectionStart({ x, y });
setSelectionEnd({ x, y });
},
[isSelectionMode],
);
const handleSelectionMove = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isSelectionMode || !selectionStart) {
return;
}
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setSelectionEnd({ x, y });
},
[isSelectionMode, selectionStart],
);
if (!isSelectionMode) {
return null;
}
return (
<div
className="absolute inset-0 cursor-crosshair"
onMouseDown={handleSelectionStart}
onMouseMove={handleSelectionMove}
onMouseUp={handleCopySelection}
onMouseLeave={() => {
if (selectionStart) {
setSelectionStart(null);
}
}}
style={{
backgroundColor: isCapturing ? 'transparent' : 'rgba(0, 0, 0, 0.1)',
userSelect: 'none',
WebkitUserSelect: 'none',
pointerEvents: 'all',
opacity: isCapturing ? 0 : 1,
zIndex: 50,
transition: 'opacity 0.1s ease-in-out',
}}
>
{selectionStart && selectionEnd && !isCapturing && (
<div
className="absolute border-2 border-blue-500 bg-blue-200 bg-opacity-20"
style={{
left: Math.min(selectionStart.x, selectionEnd.x),
top: Math.min(selectionStart.y, selectionEnd.y),
width: Math.abs(selectionEnd.x - selectionStart.x),
height: Math.abs(selectionEnd.y - selectionStart.y),
}}
/>
)}
</div>
);
},
);

View File

@@ -39,6 +39,8 @@ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Re
return env.TOGETHER_API_KEY || cloudflareEnv.TOGETHER_API_KEY;
case 'xAI':
return env.XAI_API_KEY || cloudflareEnv.XAI_API_KEY;
case 'Perplexity':
return env.PERPLEXITY_API_KEY || cloudflareEnv.PERPLEXITY_API_KEY;
case 'Cohere':
return env.COHERE_API_KEY;
case 'AzureOpenAI':

View File

@@ -128,6 +128,15 @@ export function getXAIModel(apiKey: OptionalApiKey, model: string) {
return openai(model);
}
export function getPerplexityModel(apiKey: OptionalApiKey, model: string) {
const perplexity = createOpenAI({
baseURL: 'https://api.perplexity.ai/',
apiKey,
});
return perplexity(model);
}
export function getModel(
provider: string,
model: string,
@@ -170,6 +179,8 @@ export function getModel(
return getXAIModel(apiKey, model);
case 'Cohere':
return getCohereAIModel(apiKey, model);
case 'Perplexity':
return getPerplexityModel(apiKey, model);
default:
return getOllamaModel(baseURL, model);
}

View File

@@ -139,11 +139,12 @@ const PROVIDER_LIST: ProviderInfo[] = [
{
name: 'Groq',
staticModels: [
{ name: 'llama-3.1-70b-versatile', label: 'Llama 3.1 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-90b-vision-preview', label: 'Llama 3.2 90b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
],
getApiKeyLink: 'https://console.groq.com/keys',
},
@@ -292,6 +293,30 @@ const PROVIDER_LIST: ProviderInfo[] = [
],
getApiKeyLink: 'https://api.together.xyz/settings/api-keys',
},
{
name: 'Perplexity',
staticModels: [
{
name: 'llama-3.1-sonar-small-128k-online',
label: 'Sonar Small Online',
provider: 'Perplexity',
maxTokenAllowed: 8192,
},
{
name: 'llama-3.1-sonar-large-128k-online',
label: 'Sonar Large Online',
provider: 'Perplexity',
maxTokenAllowed: 8192,
},
{
name: 'llama-3.1-sonar-huge-128k-online',
label: 'Sonar Huge Online',
provider: 'Perplexity',
maxTokenAllowed: 8192,
},
],
getApiKeyLink: 'https://www.perplexity.ai/settings/api',
},
];
export const DEFAULT_PROVIDER = PROVIDER_LIST[0];