Merge branch 'main' into FEAT_BoltDYI_NEW_SETTINGS_UI_V2
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import ignore from 'ignore';
|
||||
import { useGit } from '~/lib/hooks/useGit';
|
||||
import type { Message } from 'ai';
|
||||
import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
|
||||
import { detectProjectCommands, createCommandsMessage, escapeBoltTags } from '~/utils/projectCommands';
|
||||
import { generateId } from '~/utils/fileUtils';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
|
||||
import type { IChatMetadata } from '~/lib/persistence';
|
||||
|
||||
const IGNORE_PATTERNS = [
|
||||
'node_modules/**',
|
||||
@@ -35,7 +36,7 @@ const ig = ignore().add(IGNORE_PATTERNS);
|
||||
|
||||
interface GitCloneButtonProps {
|
||||
className?: string;
|
||||
importChat?: (description: string, messages: Message[]) => Promise<void>;
|
||||
importChat?: (description: string, messages: Message[], metadata?: IChatMetadata) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
|
||||
@@ -83,7 +84,7 @@ ${fileContents
|
||||
.map(
|
||||
(file) =>
|
||||
`<boltAction type="file" filePath="${file.path}">
|
||||
${file.content}
|
||||
${escapeBoltTags(file.content)}
|
||||
</boltAction>`,
|
||||
)
|
||||
.join('\n')}
|
||||
@@ -98,7 +99,7 @@ ${file.content}
|
||||
messages.push(commandsMessage);
|
||||
}
|
||||
|
||||
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
|
||||
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages, { gitUrl: repoUrl });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during import:', error);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Artifact } from './Artifact';
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
|
||||
import styles from './Markdown.module.scss';
|
||||
import ThoughtBox from './ThoughtBox';
|
||||
|
||||
const logger = createScopedLogger('MarkdownComponent');
|
||||
|
||||
@@ -22,6 +23,8 @@ export const Markdown = memo(({ children, html = false, limitedMarkdown = false
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
div: ({ className, children, node, ...props }) => {
|
||||
console.log(className, node);
|
||||
|
||||
if (className?.includes('__boltArtifact__')) {
|
||||
const messageId = node?.properties.dataMessageId as string;
|
||||
|
||||
@@ -32,6 +35,10 @@ export const Markdown = memo(({ children, html = false, limitedMarkdown = false
|
||||
return <Artifact messageId={messageId} />;
|
||||
}
|
||||
|
||||
if (className?.includes('__boltThought__')) {
|
||||
return <ThoughtBox title="Thought process">{children}</ThoughtBox>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
|
||||
43
app/components/chat/ThoughtBox.tsx
Normal file
43
app/components/chat/ThoughtBox.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useState, type PropsWithChildren } from 'react';
|
||||
|
||||
const ThoughtBox = ({ title, children }: PropsWithChildren<{ title: string }>) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className={`
|
||||
bg-bolt-elements-background-depth-2
|
||||
shadow-md
|
||||
rounded-lg
|
||||
cursor-pointer
|
||||
transition-all
|
||||
duration-300
|
||||
${isExpanded ? 'max-h-96' : 'max-h-13'}
|
||||
overflow-auto
|
||||
border border-bolt-elements-borderColor
|
||||
`}
|
||||
>
|
||||
<div className="p-4 flex items-center gap-4 rounded-lg text-bolt-elements-textSecondary font-medium leading-5 text-sm border border-bolt-elements-borderColor">
|
||||
<div className="i-ph:brain-thin text-2xl" />
|
||||
<div className="div">
|
||||
<span> {title}</span>{' '}
|
||||
{!isExpanded && <span className="text-bolt-elements-textTertiary"> - Click to expand</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
transition-opacity
|
||||
duration-300
|
||||
p-4
|
||||
rounded-lg
|
||||
${isExpanded ? 'opacity-100' : 'opacity-0'}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThoughtBox;
|
||||
@@ -7,7 +7,7 @@ import { BaseChat } from '~/components/chat/BaseChat';
|
||||
import { Chat } from '~/components/chat/Chat.client';
|
||||
import { useGit } from '~/lib/hooks/useGit';
|
||||
import { useChatHistory } from '~/lib/persistence';
|
||||
import { createCommandsMessage, detectProjectCommands } from '~/utils/projectCommands';
|
||||
import { createCommandsMessage, detectProjectCommands, escapeBoltTags } from '~/utils/projectCommands';
|
||||
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -74,12 +74,12 @@ export function GitUrlImport() {
|
||||
const filesMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: `Cloning the repo ${repoUrl} into ${workdir}
|
||||
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
|
||||
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
|
||||
${fileContents
|
||||
.map(
|
||||
(file) =>
|
||||
`<boltAction type="file" filePath="${file.path}">
|
||||
${file.content}
|
||||
${escapeBoltTags(file.content)}
|
||||
</boltAction>`,
|
||||
)
|
||||
.join('\n')}
|
||||
@@ -94,7 +94,7 @@ ${file.content}
|
||||
messages.push(commandsMessage);
|
||||
}
|
||||
|
||||
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
|
||||
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages, { gitUrl: repoUrl });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during import:', error);
|
||||
|
||||
@@ -18,6 +18,7 @@ import { EditorPanel } from './EditorPanel';
|
||||
import { Preview } from './Preview';
|
||||
import useViewport from '~/lib/hooks';
|
||||
import Cookies from 'js-cookie';
|
||||
import { chatMetadata, useChatHistory } from '~/lib/persistence';
|
||||
|
||||
interface WorkspaceProps {
|
||||
chatStarted?: boolean;
|
||||
@@ -66,6 +67,8 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
||||
const unsavedFiles = useStore(workbenchStore.unsavedFiles);
|
||||
const files = useStore(workbenchStore.files);
|
||||
const selectedView = useStore(workbenchStore.currentView);
|
||||
const metadata = useStore(chatMetadata);
|
||||
const { updateChatMestaData } = useChatHistory();
|
||||
|
||||
const isSmallViewport = useViewport(1024);
|
||||
|
||||
@@ -171,18 +174,28 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={() => {
|
||||
const repoName = prompt(
|
||||
'Please enter a name for your new GitHub repository:',
|
||||
'bolt-generated-project',
|
||||
);
|
||||
let repoName = metadata?.gitUrl?.split('/').slice(-1)[0]?.replace('.git', '') || null;
|
||||
let repoConfirmed: boolean = true;
|
||||
|
||||
if (repoName) {
|
||||
repoConfirmed = confirm(`Do you want to push to the repository ${repoName}?`);
|
||||
}
|
||||
|
||||
if (!repoName || !repoConfirmed) {
|
||||
repoName = prompt(
|
||||
'Please enter a name for your new GitHub repository:',
|
||||
'bolt-generated-project',
|
||||
);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!repoName) {
|
||||
alert('Repository name is required. Push to GitHub cancelled.');
|
||||
return;
|
||||
}
|
||||
|
||||
const githubUsername = Cookies.get('githubUsername');
|
||||
const githubToken = Cookies.get('githubToken');
|
||||
let githubUsername = Cookies.get('githubUsername');
|
||||
let githubToken = Cookies.get('githubToken');
|
||||
|
||||
if (!githubUsername || !githubToken) {
|
||||
const usernameInput = prompt('Please enter your GitHub username:');
|
||||
@@ -193,9 +206,26 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
||||
return;
|
||||
}
|
||||
|
||||
workbenchStore.pushToGitHub(repoName, usernameInput, tokenInput);
|
||||
} else {
|
||||
workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
|
||||
githubUsername = usernameInput;
|
||||
githubToken = tokenInput;
|
||||
|
||||
Cookies.set('githubUsername', usernameInput);
|
||||
Cookies.set('githubToken', tokenInput);
|
||||
Cookies.set(
|
||||
'git:github.com',
|
||||
JSON.stringify({ username: tokenInput, password: 'x-oauth-basic' }),
|
||||
);
|
||||
}
|
||||
|
||||
const commitMessage =
|
||||
prompt('Please enter a commit message:', 'Initial commit') || 'Initial commit';
|
||||
workbenchStore.pushToGitHub(repoName, commitMessage, githubUsername, githubToken);
|
||||
|
||||
if (!metadata?.gitUrl) {
|
||||
updateChatMestaData({
|
||||
...(metadata || {}),
|
||||
gitUrl: `https://github.com/${githubUsername}/${repoName}.git`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -92,6 +92,7 @@ export function useGit() {
|
||||
},
|
||||
onAuthFailure: (url, _auth) => {
|
||||
toast.error(`Error Authenticating with ${url.split('/')[2]}`);
|
||||
throw `Error Authenticating with ${url.split('/')[2]}`;
|
||||
},
|
||||
onAuthSuccess: (url, auth) => {
|
||||
saveGitAuth(url, auth);
|
||||
@@ -107,6 +108,8 @@ export function useGit() {
|
||||
return { workdir: webcontainer.workdir, data };
|
||||
} catch (error) {
|
||||
console.error('Git clone error:', error);
|
||||
|
||||
// toast.error(`Git clone error ${(error as any).message||""}`);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BaseProvider } from '~/lib/modules/llm/base-provider';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import type { LanguageModelV1 } from 'ai';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { createDeepSeek } from '@ai-sdk/deepseek';
|
||||
|
||||
export default class DeepseekProvider extends BaseProvider {
|
||||
name = 'Deepseek';
|
||||
@@ -38,11 +38,12 @@ export default class DeepseekProvider extends BaseProvider {
|
||||
throw new Error(`Missing API key for ${this.name} provider`);
|
||||
}
|
||||
|
||||
const openai = createOpenAI({
|
||||
baseURL: 'https://api.deepseek.com/beta',
|
||||
const deepseek = createDeepSeek({
|
||||
apiKey,
|
||||
});
|
||||
|
||||
return openai(model);
|
||||
return deepseek(model, {
|
||||
// simulateStreaming: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export default class GroqProvider extends BaseProvider {
|
||||
{ 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 },
|
||||
{ name: 'deepseek-r1-distill-llama-70b', label: 'Deepseek R1 Distill Llama 70b (Groq)', provider: 'Groq', maxTokenAllowed: 131072 },
|
||||
];
|
||||
|
||||
getModelInstance(options: {
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class LMStudioProvider extends BaseProvider {
|
||||
* Running in Server
|
||||
* Backend: Check if we're running in Docker
|
||||
*/
|
||||
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
|
||||
const isDocker = process?.env?.RUNNING_IN_DOCKER === 'true' || serverEnv?.RUNNING_IN_DOCKER === 'true';
|
||||
|
||||
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
|
||||
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
|
||||
@@ -58,7 +58,7 @@ export default class LMStudioProvider extends BaseProvider {
|
||||
}
|
||||
getModelInstance: (options: {
|
||||
model: string;
|
||||
serverEnv: Env;
|
||||
serverEnv?: Env;
|
||||
apiKeys?: Record<string, string>;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
}) => LanguageModelV1 = (options) => {
|
||||
@@ -75,8 +75,9 @@ export default class LMStudioProvider extends BaseProvider {
|
||||
throw new Error('No baseUrl found for LMStudio provider');
|
||||
}
|
||||
|
||||
const isDocker = process.env.RUNNING_IN_DOCKER === 'true' || serverEnv?.RUNNING_IN_DOCKER === 'true';
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
|
||||
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
|
||||
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
|
||||
}
|
||||
@@ -84,7 +85,7 @@ export default class LMStudioProvider extends BaseProvider {
|
||||
logger.debug('LMStudio Base Url used: ', baseUrl);
|
||||
|
||||
const lmstudio = createOpenAI({
|
||||
baseUrl: `${baseUrl}/v1`,
|
||||
baseURL: `${baseUrl}/v1`,
|
||||
apiKey: '',
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export default class OllamaProvider extends BaseProvider {
|
||||
* Running in Server
|
||||
* Backend: Check if we're running in Docker
|
||||
*/
|
||||
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
|
||||
const isDocker = process?.env?.RUNNING_IN_DOCKER === 'true' || serverEnv?.RUNNING_IN_DOCKER === 'true';
|
||||
|
||||
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
|
||||
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
|
||||
@@ -83,7 +83,7 @@ export default class OllamaProvider extends BaseProvider {
|
||||
}
|
||||
getModelInstance: (options: {
|
||||
model: string;
|
||||
serverEnv: Env;
|
||||
serverEnv?: Env;
|
||||
apiKeys?: Record<string, string>;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
}) => LanguageModelV1 = (options) => {
|
||||
@@ -101,7 +101,7 @@ export default class OllamaProvider extends BaseProvider {
|
||||
throw new Error('No baseUrl found for OLLAMA provider');
|
||||
}
|
||||
|
||||
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
|
||||
const isDocker = process?.env?.RUNNING_IN_DOCKER === 'true' || serverEnv?.RUNNING_IN_DOCKER === 'true';
|
||||
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
|
||||
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@ import type { Message } from 'ai';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import type { ChatHistoryItem } from './useChatHistory';
|
||||
|
||||
export interface IChatMetadata {
|
||||
gitUrl: string;
|
||||
gitBranch?: string;
|
||||
}
|
||||
|
||||
const logger = createScopedLogger('ChatHistory');
|
||||
|
||||
// this is used at the top level and never rejects
|
||||
@@ -53,6 +58,7 @@ export async function setMessages(
|
||||
urlId?: string,
|
||||
description?: string,
|
||||
timestamp?: string,
|
||||
metadata?: IChatMetadata,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction('chats', 'readwrite');
|
||||
@@ -69,6 +75,7 @@ export async function setMessages(
|
||||
urlId,
|
||||
description,
|
||||
timestamp: timestamp ?? new Date().toISOString(),
|
||||
metadata,
|
||||
});
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
@@ -204,6 +211,7 @@ export async function createChatFromMessages(
|
||||
db: IDBDatabase,
|
||||
description: string,
|
||||
messages: Message[],
|
||||
metadata?: IChatMetadata,
|
||||
): Promise<string> {
|
||||
const newId = await getNextId(db);
|
||||
const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat
|
||||
@@ -214,6 +222,8 @@ export async function createChatFromMessages(
|
||||
messages,
|
||||
newUrlId, // Use the new urlId
|
||||
description,
|
||||
undefined, // Use the current timestamp
|
||||
metadata,
|
||||
);
|
||||
|
||||
return newUrlId; // Return the urlId instead of id for navigation
|
||||
@@ -230,5 +240,19 @@ export async function updateChatDescription(db: IDBDatabase, id: string, descrip
|
||||
throw new Error('Description cannot be empty');
|
||||
}
|
||||
|
||||
await setMessages(db, id, chat.messages, chat.urlId, description, chat.timestamp);
|
||||
await setMessages(db, id, chat.messages, chat.urlId, description, chat.timestamp, chat.metadata);
|
||||
}
|
||||
|
||||
export async function updateChatMetadata(
|
||||
db: IDBDatabase,
|
||||
id: string,
|
||||
metadata: IChatMetadata | undefined,
|
||||
): Promise<void> {
|
||||
const chat = await getMessages(db, id);
|
||||
|
||||
if (!chat) {
|
||||
throw new Error('Chat not found');
|
||||
}
|
||||
|
||||
await setMessages(db, id, chat.messages, chat.urlId, chat.description, chat.timestamp, metadata);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
setMessages,
|
||||
duplicateChat,
|
||||
createChatFromMessages,
|
||||
type IChatMetadata,
|
||||
} from './db';
|
||||
|
||||
export interface ChatHistoryItem {
|
||||
@@ -21,6 +22,7 @@ export interface ChatHistoryItem {
|
||||
description?: string;
|
||||
messages: Message[];
|
||||
timestamp: string;
|
||||
metadata?: IChatMetadata;
|
||||
}
|
||||
|
||||
const persistenceEnabled = !import.meta.env.VITE_DISABLE_PERSISTENCE;
|
||||
@@ -29,7 +31,7 @@ export const db = persistenceEnabled ? await openDatabase() : undefined;
|
||||
|
||||
export const chatId = atom<string | undefined>(undefined);
|
||||
export const description = atom<string | undefined>(undefined);
|
||||
|
||||
export const chatMetadata = atom<IChatMetadata | undefined>(undefined);
|
||||
export function useChatHistory() {
|
||||
const navigate = useNavigate();
|
||||
const { id: mixedId } = useLoaderData<{ id?: string }>();
|
||||
@@ -65,6 +67,7 @@ export function useChatHistory() {
|
||||
setUrlId(storedMessages.urlId);
|
||||
description.set(storedMessages.description);
|
||||
chatId.set(storedMessages.id);
|
||||
chatMetadata.set(storedMessages.metadata);
|
||||
} else {
|
||||
navigate('/', { replace: true });
|
||||
}
|
||||
@@ -81,6 +84,21 @@ export function useChatHistory() {
|
||||
return {
|
||||
ready: !mixedId || ready,
|
||||
initialMessages,
|
||||
updateChatMestaData: async (metadata: IChatMetadata) => {
|
||||
const id = chatId.get();
|
||||
|
||||
if (!db || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await setMessages(db, id, initialMessages, urlId, description.get(), undefined, metadata);
|
||||
chatMetadata.set(metadata);
|
||||
} catch (error) {
|
||||
toast.error('Failed to update chat metadata');
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
storeMessageHistory: async (messages: Message[]) => {
|
||||
if (!db || messages.length === 0) {
|
||||
return;
|
||||
@@ -109,7 +127,7 @@ export function useChatHistory() {
|
||||
}
|
||||
}
|
||||
|
||||
await setMessages(db, chatId.get() as string, messages, urlId, description.get());
|
||||
await setMessages(db, chatId.get() as string, messages, urlId, description.get(), undefined, chatMetadata.get());
|
||||
},
|
||||
duplicateCurrentChat: async (listItemId: string) => {
|
||||
if (!db || (!mixedId && !listItemId)) {
|
||||
@@ -125,13 +143,13 @@ export function useChatHistory() {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
importChat: async (description: string, messages: Message[]) => {
|
||||
importChat: async (description: string, messages: Message[], metadata?: IChatMetadata) => {
|
||||
if (!db) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newId = await createChatFromMessages(db, description, messages);
|
||||
const newId = await createChatFromMessages(db, description, messages, metadata);
|
||||
window.location.href = `/chat/${newId}`;
|
||||
toast.success('Chat imported successfully');
|
||||
} catch (error) {
|
||||
|
||||
@@ -64,6 +64,10 @@ function cleanoutMarkdownSyntax(content: string) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanEscapedTags(content: string) {
|
||||
return content.replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
export class StreamingMessageParser {
|
||||
#messages = new Map<string, MessageState>();
|
||||
|
||||
@@ -110,6 +114,7 @@ export class StreamingMessageParser {
|
||||
// Remove markdown code block syntax if present and file is not markdown
|
||||
if (!currentAction.filePath.endsWith('.md')) {
|
||||
content = cleanoutMarkdownSyntax(content);
|
||||
content = cleanEscapedTags(content);
|
||||
}
|
||||
|
||||
content += '\n';
|
||||
@@ -141,6 +146,7 @@ export class StreamingMessageParser {
|
||||
|
||||
if (!currentAction.filePath.endsWith('.md')) {
|
||||
content = cleanoutMarkdownSyntax(content);
|
||||
content = cleanEscapedTags(content);
|
||||
}
|
||||
|
||||
this._options.callbacks?.onActionStream?.({
|
||||
|
||||
@@ -434,7 +434,7 @@ export class WorkbenchStore {
|
||||
return syncedFiles;
|
||||
}
|
||||
|
||||
async pushToGitHub(repoName: string, githubUsername?: string, ghToken?: string) {
|
||||
async pushToGitHub(repoName: string, commitMessage?: string, githubUsername?: string, ghToken?: string) {
|
||||
try {
|
||||
// Use cookies if username and token are not provided
|
||||
const githubToken = ghToken || Cookies.get('githubToken');
|
||||
@@ -523,7 +523,7 @@ export class WorkbenchStore {
|
||||
const { data: newCommit } = await octokit.git.createCommit({
|
||||
owner: repo.owner.login,
|
||||
repo: repo.name,
|
||||
message: 'Initial commit from your app',
|
||||
message: commitMessage || 'Initial commit from your app',
|
||||
tree: newTree.sha,
|
||||
parents: [latestCommitSha],
|
||||
});
|
||||
|
||||
@@ -63,6 +63,8 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
||||
const totalMessageContent = messages.reduce((acc, message) => acc + message.content, '');
|
||||
logger.debug(`Total message length: ${totalMessageContent.split(' ').length}, words`);
|
||||
|
||||
let lastChunk: string | undefined = undefined;
|
||||
|
||||
const dataStream = createDataStream({
|
||||
async execute(dataStream) {
|
||||
const filePaths = getFilePaths(files || {});
|
||||
@@ -247,15 +249,42 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
result.mergeIntoDataStream(dataStream);
|
||||
},
|
||||
onError: (error: any) => `Custom error: ${error.message}`,
|
||||
}).pipeThrough(
|
||||
new TransformStream({
|
||||
transform: (chunk, controller) => {
|
||||
if (!lastChunk) {
|
||||
lastChunk = ' ';
|
||||
}
|
||||
|
||||
if (typeof chunk === 'string') {
|
||||
if (chunk.startsWith('g') && !lastChunk.startsWith('g')) {
|
||||
controller.enqueue(encoder.encode(`0: "<div class=\\"__boltThought__\\">"\n`));
|
||||
}
|
||||
|
||||
if (lastChunk.startsWith('g') && !chunk.startsWith('g')) {
|
||||
controller.enqueue(encoder.encode(`0: "</div>\\n"\n`));
|
||||
}
|
||||
}
|
||||
|
||||
lastChunk = chunk;
|
||||
|
||||
let transformedChunk = chunk;
|
||||
|
||||
if (typeof chunk === 'string' && chunk.startsWith('g')) {
|
||||
let content = chunk.split(':').slice(1).join(':');
|
||||
|
||||
if (content.endsWith('\n')) {
|
||||
content = content.slice(0, content.length - 1);
|
||||
}
|
||||
|
||||
transformedChunk = `0:${content}\n`;
|
||||
}
|
||||
|
||||
// Convert the string stream to a byte stream
|
||||
const str = typeof chunk === 'string' ? chunk : JSON.stringify(chunk);
|
||||
const str = typeof transformedChunk === 'string' ? transformedChunk : JSON.stringify(transformedChunk);
|
||||
controller.enqueue(encoder.encode(str));
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -41,11 +41,17 @@ function getProviderInfo(llmManager: LLMManager) {
|
||||
export async function loader({
|
||||
request,
|
||||
params,
|
||||
context,
|
||||
}: {
|
||||
request: Request;
|
||||
params: { provider?: string };
|
||||
context: {
|
||||
cloudflare?: {
|
||||
env: Record<string, string>;
|
||||
};
|
||||
};
|
||||
}): Promise<Response> {
|
||||
const llmManager = LLMManager.getInstance(import.meta.env);
|
||||
const llmManager = LLMManager.getInstance(context.cloudflare?.env);
|
||||
|
||||
// Get client side maintained API keys and provider settings from cookies
|
||||
const cookieHeader = request.headers.get('Cookie');
|
||||
@@ -63,7 +69,7 @@ export async function loader({
|
||||
if (provider) {
|
||||
const staticModels = provider.staticModels;
|
||||
const dynamicModels = provider.getDynamicModels
|
||||
? await provider.getDynamicModels(apiKeys, providerSettings, import.meta.env)
|
||||
? await provider.getDynamicModels(apiKeys, providerSettings, context.cloudflare?.env)
|
||||
: [];
|
||||
modelList = [...staticModels, ...dynamicModels];
|
||||
}
|
||||
@@ -72,7 +78,7 @@ export async function loader({
|
||||
modelList = await llmManager.updateModelList({
|
||||
apiKeys,
|
||||
providerSettings,
|
||||
serverEnv: import.meta.env,
|
||||
serverEnv: context.cloudflare?.env,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Message } from 'ai';
|
||||
import { generateId } from './fileUtils';
|
||||
import { detectProjectCommands, createCommandsMessage } from './projectCommands';
|
||||
import { detectProjectCommands, createCommandsMessage, escapeBoltTags } from './projectCommands';
|
||||
|
||||
export const createChatFromFolder = async (
|
||||
files: File[],
|
||||
@@ -42,7 +42,7 @@ export const createChatFromFolder = async (
|
||||
${fileArtifacts
|
||||
.map(
|
||||
(file) => `<boltAction type="file" filePath="${file.path}">
|
||||
${file.content}
|
||||
${escapeBoltTags(file.content)}
|
||||
</boltAction>`,
|
||||
)
|
||||
.join('\n\n')}
|
||||
|
||||
@@ -61,7 +61,13 @@ const rehypeSanitizeOptions: RehypeSanitizeOptions = {
|
||||
tagNames: allowedHTMLElements,
|
||||
attributes: {
|
||||
...defaultSchema.attributes,
|
||||
div: [...(defaultSchema.attributes?.div ?? []), 'data*', ['className', '__boltArtifact__']],
|
||||
div: [
|
||||
...(defaultSchema.attributes?.div ?? []),
|
||||
'data*',
|
||||
['className', '__boltArtifact__', '__boltThought__'],
|
||||
|
||||
// ['className', '__boltThought__']
|
||||
],
|
||||
},
|
||||
strip: [],
|
||||
};
|
||||
|
||||
@@ -78,3 +78,39 @@ ${commands.setupCommand}
|
||||
createdAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
export function escapeBoltArtifactTags(input: string) {
|
||||
// Regular expression to match boltArtifact tags and their content
|
||||
const regex = /(<boltArtifact[^>]*>)([\s\S]*?)(<\/boltArtifact>)/g;
|
||||
|
||||
return input.replace(regex, (match, openTag, content, closeTag) => {
|
||||
// Escape the opening tag
|
||||
const escapedOpenTag = openTag.replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// Escape the closing tag
|
||||
const escapedCloseTag = closeTag.replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// Return the escaped version
|
||||
return `${escapedOpenTag}${content}${escapedCloseTag}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function escapeBoltAActionTags(input: string) {
|
||||
// Regular expression to match boltArtifact tags and their content
|
||||
const regex = /(<boltAction[^>]*>)([\s\S]*?)(<\/boltAction>)/g;
|
||||
|
||||
return input.replace(regex, (match, openTag, content, closeTag) => {
|
||||
// Escape the opening tag
|
||||
const escapedOpenTag = openTag.replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// Escape the closing tag
|
||||
const escapedCloseTag = closeTag.replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// Return the escaped version
|
||||
return `${escapedOpenTag}${content}${escapedCloseTag}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function escapeBoltTags(input: string) {
|
||||
return escapeBoltArtifactTags(escapeBoltAActionTags(input));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user