feat: added Automatic Code Template Detection And Import (#867)
Browse files* initial setup
* updated template list
* added optional switch to control this feature
* removed some logs
- app/components/chat/Chat.client.tsx +109 -3
- app/components/chat/Messages.client.tsx +7 -2
- app/components/settings/features/FeaturesTab.tsx +22 -7
- app/lib/hooks/useSettings.tsx +16 -0
- app/lib/runtime/message-parser.ts +0 -1
- app/lib/stores/settings.ts +1 -0
- app/routes/api.llmcall.ts +163 -0
- app/utils/selectStarterTemplate.ts +290 -0
app/components/chat/Chat.client.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import { useSettings } from '~/lib/hooks/useSettings';
|
|
| 22 |
import type { ProviderInfo } from '~/types/model';
|
| 23 |
import { useSearchParams } from '@remix-run/react';
|
| 24 |
import { createSampler } from '~/utils/sampler';
|
|
|
|
| 25 |
|
| 26 |
const toastAnimation = cssTransition({
|
| 27 |
enter: 'animated fadeInRight',
|
|
@@ -116,9 +117,10 @@ export const ChatImpl = memo(
|
|
| 116 |
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
|
| 117 |
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
|
| 118 |
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
| 119 |
const files = useStore(workbenchStore.files);
|
| 120 |
const actionAlert = useStore(workbenchStore.alert);
|
| 121 |
-
const { activeProviders, promptId, contextOptimizationEnabled } = useSettings();
|
| 122 |
|
| 123 |
const [model, setModel] = useState(() => {
|
| 124 |
const savedModel = Cookies.get('selectedModel');
|
|
@@ -135,7 +137,7 @@ export const ChatImpl = memo(
|
|
| 135 |
|
| 136 |
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
| 137 |
|
| 138 |
-
const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
|
| 139 |
api: '/api/chat',
|
| 140 |
body: {
|
| 141 |
apiKeys,
|
|
@@ -266,6 +268,110 @@ export const ChatImpl = memo(
|
|
| 266 |
|
| 267 |
runAnimation();
|
| 268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
if (fileModifications !== undefined) {
|
| 270 |
/**
|
| 271 |
* If we have file modifications we append a new user message manually since we have to prefix
|
|
@@ -368,7 +474,7 @@ export const ChatImpl = memo(
|
|
| 368 |
input={input}
|
| 369 |
showChat={showChat}
|
| 370 |
chatStarted={chatStarted}
|
| 371 |
-
isStreaming={isLoading}
|
| 372 |
enhancingPrompt={enhancingPrompt}
|
| 373 |
promptEnhanced={promptEnhanced}
|
| 374 |
sendMessage={sendMessage}
|
|
|
|
| 22 |
import type { ProviderInfo } from '~/types/model';
|
| 23 |
import { useSearchParams } from '@remix-run/react';
|
| 24 |
import { createSampler } from '~/utils/sampler';
|
| 25 |
+
import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
|
| 26 |
|
| 27 |
const toastAnimation = cssTransition({
|
| 28 |
enter: 'animated fadeInRight',
|
|
|
|
| 117 |
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
|
| 118 |
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
|
| 119 |
const [searchParams, setSearchParams] = useSearchParams();
|
| 120 |
+
const [fakeLoading, setFakeLoading] = useState(false);
|
| 121 |
const files = useStore(workbenchStore.files);
|
| 122 |
const actionAlert = useStore(workbenchStore.alert);
|
| 123 |
+
const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings();
|
| 124 |
|
| 125 |
const [model, setModel] = useState(() => {
|
| 126 |
const savedModel = Cookies.get('selectedModel');
|
|
|
|
| 137 |
|
| 138 |
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
| 139 |
|
| 140 |
+
const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload } = useChat({
|
| 141 |
api: '/api/chat',
|
| 142 |
body: {
|
| 143 |
apiKeys,
|
|
|
|
| 268 |
|
| 269 |
runAnimation();
|
| 270 |
|
| 271 |
+
if (!chatStarted && messageInput && autoSelectTemplate) {
|
| 272 |
+
setFakeLoading(true);
|
| 273 |
+
setMessages([
|
| 274 |
+
{
|
| 275 |
+
id: `${new Date().getTime()}`,
|
| 276 |
+
role: 'user',
|
| 277 |
+
content: [
|
| 278 |
+
{
|
| 279 |
+
type: 'text',
|
| 280 |
+
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
|
| 281 |
+
},
|
| 282 |
+
...imageDataList.map((imageData) => ({
|
| 283 |
+
type: 'image',
|
| 284 |
+
image: imageData,
|
| 285 |
+
})),
|
| 286 |
+
] as any, // Type assertion to bypass compiler check
|
| 287 |
+
},
|
| 288 |
+
]);
|
| 289 |
+
|
| 290 |
+
// reload();
|
| 291 |
+
|
| 292 |
+
const template = await selectStarterTemplate({
|
| 293 |
+
message: messageInput,
|
| 294 |
+
model,
|
| 295 |
+
provider,
|
| 296 |
+
});
|
| 297 |
+
|
| 298 |
+
if (template !== 'blank') {
|
| 299 |
+
const temResp = await getTemplates(template);
|
| 300 |
+
|
| 301 |
+
if (temResp) {
|
| 302 |
+
const { assistantMessage, userMessage } = temResp;
|
| 303 |
+
|
| 304 |
+
setMessages([
|
| 305 |
+
{
|
| 306 |
+
id: `${new Date().getTime()}`,
|
| 307 |
+
role: 'user',
|
| 308 |
+
content: messageInput,
|
| 309 |
+
|
| 310 |
+
// annotations: ['hidden'],
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
id: `${new Date().getTime()}`,
|
| 314 |
+
role: 'assistant',
|
| 315 |
+
content: assistantMessage,
|
| 316 |
+
},
|
| 317 |
+
{
|
| 318 |
+
id: `${new Date().getTime()}`,
|
| 319 |
+
role: 'user',
|
| 320 |
+
content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userMessage}`,
|
| 321 |
+
annotations: ['hidden'],
|
| 322 |
+
},
|
| 323 |
+
]);
|
| 324 |
+
|
| 325 |
+
reload();
|
| 326 |
+
setFakeLoading(false);
|
| 327 |
+
|
| 328 |
+
return;
|
| 329 |
+
} else {
|
| 330 |
+
setMessages([
|
| 331 |
+
{
|
| 332 |
+
id: `${new Date().getTime()}`,
|
| 333 |
+
role: 'user',
|
| 334 |
+
content: [
|
| 335 |
+
{
|
| 336 |
+
type: 'text',
|
| 337 |
+
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
|
| 338 |
+
},
|
| 339 |
+
...imageDataList.map((imageData) => ({
|
| 340 |
+
type: 'image',
|
| 341 |
+
image: imageData,
|
| 342 |
+
})),
|
| 343 |
+
] as any, // Type assertion to bypass compiler check
|
| 344 |
+
},
|
| 345 |
+
]);
|
| 346 |
+
reload();
|
| 347 |
+
setFakeLoading(false);
|
| 348 |
+
|
| 349 |
+
return;
|
| 350 |
+
}
|
| 351 |
+
} else {
|
| 352 |
+
setMessages([
|
| 353 |
+
{
|
| 354 |
+
id: `${new Date().getTime()}`,
|
| 355 |
+
role: 'user',
|
| 356 |
+
content: [
|
| 357 |
+
{
|
| 358 |
+
type: 'text',
|
| 359 |
+
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
|
| 360 |
+
},
|
| 361 |
+
...imageDataList.map((imageData) => ({
|
| 362 |
+
type: 'image',
|
| 363 |
+
image: imageData,
|
| 364 |
+
})),
|
| 365 |
+
] as any, // Type assertion to bypass compiler check
|
| 366 |
+
},
|
| 367 |
+
]);
|
| 368 |
+
reload();
|
| 369 |
+
setFakeLoading(false);
|
| 370 |
+
|
| 371 |
+
return;
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
if (fileModifications !== undefined) {
|
| 376 |
/**
|
| 377 |
* If we have file modifications we append a new user message manually since we have to prefix
|
|
|
|
| 474 |
input={input}
|
| 475 |
showChat={showChat}
|
| 476 |
chatStarted={chatStarted}
|
| 477 |
+
isStreaming={isLoading || fakeLoading}
|
| 478 |
enhancingPrompt={enhancingPrompt}
|
| 479 |
promptEnhanced={promptEnhanced}
|
| 480 |
sendMessage={sendMessage}
|
app/components/chat/Messages.client.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import type { Message } from 'ai';
|
| 2 |
-
import React from 'react';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
import { AssistantMessage } from './AssistantMessage';
|
| 5 |
import { UserMessage } from './UserMessage';
|
|
@@ -44,10 +44,15 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
|
| 44 |
<div id={id} ref={ref} className={props.className}>
|
| 45 |
{messages.length > 0
|
| 46 |
? messages.map((message, index) => {
|
| 47 |
-
const { role, content, id: messageId } = message;
|
| 48 |
const isUserMessage = role === 'user';
|
| 49 |
const isFirst = index === 0;
|
| 50 |
const isLast = index === messages.length - 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
return (
|
| 53 |
<div
|
|
|
|
| 1 |
import type { Message } from 'ai';
|
| 2 |
+
import React, { Fragment } from 'react';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
import { AssistantMessage } from './AssistantMessage';
|
| 5 |
import { UserMessage } from './UserMessage';
|
|
|
|
| 44 |
<div id={id} ref={ref} className={props.className}>
|
| 45 |
{messages.length > 0
|
| 46 |
? messages.map((message, index) => {
|
| 47 |
+
const { role, content, id: messageId, annotations } = message;
|
| 48 |
const isUserMessage = role === 'user';
|
| 49 |
const isFirst = index === 0;
|
| 50 |
const isLast = index === messages.length - 1;
|
| 51 |
+
const isHidden = annotations?.includes('hidden');
|
| 52 |
+
|
| 53 |
+
if (isHidden) {
|
| 54 |
+
return <Fragment key={index} />;
|
| 55 |
+
}
|
| 56 |
|
| 57 |
return (
|
| 58 |
<div
|
app/components/settings/features/FeaturesTab.tsx
CHANGED
|
@@ -14,6 +14,8 @@ export default function FeaturesTab() {
|
|
| 14 |
enableLatestBranch,
|
| 15 |
promptId,
|
| 16 |
setPromptId,
|
|
|
|
|
|
|
| 17 |
enableContextOptimization,
|
| 18 |
contextOptimizationEnabled,
|
| 19 |
} = useSettings();
|
|
@@ -35,12 +37,21 @@ export default function FeaturesTab() {
|
|
| 35 |
<div className="flex items-center justify-between">
|
| 36 |
<div>
|
| 37 |
<span className="text-bolt-elements-textPrimary">Use Main Branch</span>
|
| 38 |
-
<p className="text-
|
| 39 |
Check for updates against the main branch instead of stable
|
| 40 |
</p>
|
| 41 |
</div>
|
| 42 |
<Switch className="ml-auto" checked={isLatestBranch} onCheckedChange={enableLatestBranch} />
|
| 43 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
<div className="flex items-center justify-between">
|
| 45 |
<div>
|
| 46 |
<span className="text-bolt-elements-textPrimary">Use Context Optimization</span>
|
|
@@ -59,18 +70,22 @@ export default function FeaturesTab() {
|
|
| 59 |
|
| 60 |
<div className="mb-6 border-t border-bolt-elements-borderColor pt-4">
|
| 61 |
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Experimental Features</h3>
|
| 62 |
-
<p className="text-sm text-bolt-elements-textSecondary mb-
|
| 63 |
Disclaimer: Experimental features may be unstable and are subject to change.
|
| 64 |
</p>
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
</div>
|
| 70 |
<div className="flex items-start justify-between pt-4 mb-2 gap-2">
|
| 71 |
<div className="flex-1 max-w-[200px]">
|
| 72 |
<span className="text-bolt-elements-textPrimary">Prompt Library</span>
|
| 73 |
-
<p className="text-
|
| 74 |
Choose a prompt from the library to use as the system prompt.
|
| 75 |
</p>
|
| 76 |
</div>
|
|
|
|
| 14 |
enableLatestBranch,
|
| 15 |
promptId,
|
| 16 |
setPromptId,
|
| 17 |
+
autoSelectTemplate,
|
| 18 |
+
setAutoSelectTemplate,
|
| 19 |
enableContextOptimization,
|
| 20 |
contextOptimizationEnabled,
|
| 21 |
} = useSettings();
|
|
|
|
| 37 |
<div className="flex items-center justify-between">
|
| 38 |
<div>
|
| 39 |
<span className="text-bolt-elements-textPrimary">Use Main Branch</span>
|
| 40 |
+
<p className="text-xs text-bolt-elements-textTertiary">
|
| 41 |
Check for updates against the main branch instead of stable
|
| 42 |
</p>
|
| 43 |
</div>
|
| 44 |
<Switch className="ml-auto" checked={isLatestBranch} onCheckedChange={enableLatestBranch} />
|
| 45 |
</div>
|
| 46 |
+
<div className="flex items-center justify-between">
|
| 47 |
+
<div>
|
| 48 |
+
<span className="text-bolt-elements-textPrimary">Auto Select Code Template</span>
|
| 49 |
+
<p className="text-xs text-bolt-elements-textTertiary">
|
| 50 |
+
Let Bolt select the best starter template for your project.
|
| 51 |
+
</p>
|
| 52 |
+
</div>
|
| 53 |
+
<Switch className="ml-auto" checked={autoSelectTemplate} onCheckedChange={setAutoSelectTemplate} />
|
| 54 |
+
</div>
|
| 55 |
<div className="flex items-center justify-between">
|
| 56 |
<div>
|
| 57 |
<span className="text-bolt-elements-textPrimary">Use Context Optimization</span>
|
|
|
|
| 70 |
|
| 71 |
<div className="mb-6 border-t border-bolt-elements-borderColor pt-4">
|
| 72 |
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Experimental Features</h3>
|
| 73 |
+
<p className="text-sm text-bolt-elements-textSecondary mb-10">
|
| 74 |
Disclaimer: Experimental features may be unstable and are subject to change.
|
| 75 |
</p>
|
| 76 |
+
<div className="flex flex-col">
|
| 77 |
+
<div className="flex items-center justify-between mb-2">
|
| 78 |
+
<span className="text-bolt-elements-textPrimary">Experimental Providers</span>
|
| 79 |
+
<Switch className="ml-auto" checked={isLocalModel} onCheckedChange={enableLocalModels} />
|
| 80 |
+
</div>
|
| 81 |
+
<p className="text-xs text-bolt-elements-textTertiary mb-4">
|
| 82 |
+
Enable experimental providers such as Ollama, LMStudio, and OpenAILike.
|
| 83 |
+
</p>
|
| 84 |
</div>
|
| 85 |
<div className="flex items-start justify-between pt-4 mb-2 gap-2">
|
| 86 |
<div className="flex-1 max-w-[200px]">
|
| 87 |
<span className="text-bolt-elements-textPrimary">Prompt Library</span>
|
| 88 |
+
<p className="text-xs text-bolt-elements-textTertiary mb-4">
|
| 89 |
Choose a prompt from the library to use as the system prompt.
|
| 90 |
</p>
|
| 91 |
</div>
|
app/lib/hooks/useSettings.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
| 7 |
promptStore,
|
| 8 |
providersStore,
|
| 9 |
latestBranchStore,
|
|
|
|
| 10 |
enableContextOptimizationStore,
|
| 11 |
} from '~/lib/stores/settings';
|
| 12 |
import { useCallback, useEffect, useState } from 'react';
|
|
@@ -31,6 +32,7 @@ export function useSettings() {
|
|
| 31 |
const promptId = useStore(promptStore);
|
| 32 |
const isLocalModel = useStore(isLocalModelsEnabled);
|
| 33 |
const isLatestBranch = useStore(latestBranchStore);
|
|
|
|
| 34 |
const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
|
| 35 |
const contextOptimizationEnabled = useStore(enableContextOptimizationStore);
|
| 36 |
|
|
@@ -121,6 +123,12 @@ export function useSettings() {
|
|
| 121 |
latestBranchStore.set(savedLatestBranch === 'true');
|
| 122 |
}
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
const savedContextOptimizationEnabled = Cookies.get('contextOptimizationEnabled');
|
| 125 |
|
| 126 |
if (savedContextOptimizationEnabled) {
|
|
@@ -187,6 +195,12 @@ export function useSettings() {
|
|
| 187 |
Cookies.set('isLatestBranch', String(enabled));
|
| 188 |
}, []);
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
const enableContextOptimization = useCallback((enabled: boolean) => {
|
| 191 |
enableContextOptimizationStore.set(enabled);
|
| 192 |
logStore.logSystem(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
|
|
@@ -207,6 +221,8 @@ export function useSettings() {
|
|
| 207 |
setPromptId,
|
| 208 |
isLatestBranch,
|
| 209 |
enableLatestBranch,
|
|
|
|
|
|
|
| 210 |
contextOptimizationEnabled,
|
| 211 |
enableContextOptimization,
|
| 212 |
};
|
|
|
|
| 7 |
promptStore,
|
| 8 |
providersStore,
|
| 9 |
latestBranchStore,
|
| 10 |
+
autoSelectStarterTemplate,
|
| 11 |
enableContextOptimizationStore,
|
| 12 |
} from '~/lib/stores/settings';
|
| 13 |
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
| 32 |
const promptId = useStore(promptStore);
|
| 33 |
const isLocalModel = useStore(isLocalModelsEnabled);
|
| 34 |
const isLatestBranch = useStore(latestBranchStore);
|
| 35 |
+
const autoSelectTemplate = useStore(autoSelectStarterTemplate);
|
| 36 |
const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
|
| 37 |
const contextOptimizationEnabled = useStore(enableContextOptimizationStore);
|
| 38 |
|
|
|
|
| 123 |
latestBranchStore.set(savedLatestBranch === 'true');
|
| 124 |
}
|
| 125 |
|
| 126 |
+
const autoSelectTemplate = Cookies.get('autoSelectTemplate');
|
| 127 |
+
|
| 128 |
+
if (autoSelectTemplate) {
|
| 129 |
+
autoSelectStarterTemplate.set(autoSelectTemplate === 'true');
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
const savedContextOptimizationEnabled = Cookies.get('contextOptimizationEnabled');
|
| 133 |
|
| 134 |
if (savedContextOptimizationEnabled) {
|
|
|
|
| 195 |
Cookies.set('isLatestBranch', String(enabled));
|
| 196 |
}, []);
|
| 197 |
|
| 198 |
+
const setAutoSelectTemplate = useCallback((enabled: boolean) => {
|
| 199 |
+
autoSelectStarterTemplate.set(enabled);
|
| 200 |
+
logStore.logSystem(`Auto select template ${enabled ? 'enabled' : 'disabled'}`);
|
| 201 |
+
Cookies.set('autoSelectTemplate', String(enabled));
|
| 202 |
+
}, []);
|
| 203 |
+
|
| 204 |
const enableContextOptimization = useCallback((enabled: boolean) => {
|
| 205 |
enableContextOptimizationStore.set(enabled);
|
| 206 |
logStore.logSystem(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
|
|
|
|
| 221 |
setPromptId,
|
| 222 |
isLatestBranch,
|
| 223 |
enableLatestBranch,
|
| 224 |
+
autoSelectTemplate,
|
| 225 |
+
setAutoSelectTemplate,
|
| 226 |
contextOptimizationEnabled,
|
| 227 |
enableContextOptimization,
|
| 228 |
};
|
app/lib/runtime/message-parser.ts
CHANGED
|
@@ -109,7 +109,6 @@ export class StreamingMessageParser {
|
|
| 109 |
// Remove markdown code block syntax if present and file is not markdown
|
| 110 |
if (!currentAction.filePath.endsWith('.md')) {
|
| 111 |
content = cleanoutMarkdownSyntax(content);
|
| 112 |
-
console.log('content after cleanup', content);
|
| 113 |
}
|
| 114 |
|
| 115 |
content += '\n';
|
|
|
|
| 109 |
// Remove markdown code block syntax if present and file is not markdown
|
| 110 |
if (!currentAction.filePath.endsWith('.md')) {
|
| 111 |
content = cleanoutMarkdownSyntax(content);
|
|
|
|
| 112 |
}
|
| 113 |
|
| 114 |
content += '\n';
|
app/lib/stores/settings.ts
CHANGED
|
@@ -54,4 +54,5 @@ export const promptStore = atom<string>('default');
|
|
| 54 |
|
| 55 |
export const latestBranchStore = atom(false);
|
| 56 |
|
|
|
|
| 57 |
export const enableContextOptimizationStore = atom(false);
|
|
|
|
| 54 |
|
| 55 |
export const latestBranchStore = atom(false);
|
| 56 |
|
| 57 |
+
export const autoSelectStarterTemplate = atom(true);
|
| 58 |
export const enableContextOptimizationStore = atom(false);
|
app/routes/api.llmcall.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
| 2 |
+
|
| 3 |
+
//import { StreamingTextResponse, parseStreamPart } from 'ai';
|
| 4 |
+
import { streamText } from '~/lib/.server/llm/stream-text';
|
| 5 |
+
import type { IProviderSetting, ProviderInfo } from '~/types/model';
|
| 6 |
+
import { generateText } from 'ai';
|
| 7 |
+
import { getModelList, PROVIDER_LIST } from '~/utils/constants';
|
| 8 |
+
import { MAX_TOKENS } from '~/lib/.server/llm/constants';
|
| 9 |
+
|
| 10 |
+
export async function action(args: ActionFunctionArgs) {
|
| 11 |
+
return llmCallAction(args);
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
function parseCookies(cookieHeader: string) {
|
| 15 |
+
const cookies: any = {};
|
| 16 |
+
|
| 17 |
+
// Split the cookie string by semicolons and spaces
|
| 18 |
+
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
|
| 19 |
+
|
| 20 |
+
items.forEach((item) => {
|
| 21 |
+
const [name, ...rest] = item.split('=');
|
| 22 |
+
|
| 23 |
+
if (name && rest) {
|
| 24 |
+
// Decode the name and value, and join value parts in case it contains '='
|
| 25 |
+
const decodedName = decodeURIComponent(name.trim());
|
| 26 |
+
const decodedValue = decodeURIComponent(rest.join('=').trim());
|
| 27 |
+
cookies[decodedName] = decodedValue;
|
| 28 |
+
}
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
return cookies;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
| 35 |
+
const { system, message, model, provider, streamOutput } = await request.json<{
|
| 36 |
+
system: string;
|
| 37 |
+
message: string;
|
| 38 |
+
model: string;
|
| 39 |
+
provider: ProviderInfo;
|
| 40 |
+
streamOutput?: boolean;
|
| 41 |
+
}>();
|
| 42 |
+
|
| 43 |
+
const { name: providerName } = provider;
|
| 44 |
+
|
| 45 |
+
// validate 'model' and 'provider' fields
|
| 46 |
+
if (!model || typeof model !== 'string') {
|
| 47 |
+
throw new Response('Invalid or missing model', {
|
| 48 |
+
status: 400,
|
| 49 |
+
statusText: 'Bad Request',
|
| 50 |
+
});
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
if (!providerName || typeof providerName !== 'string') {
|
| 54 |
+
throw new Response('Invalid or missing provider', {
|
| 55 |
+
status: 400,
|
| 56 |
+
statusText: 'Bad Request',
|
| 57 |
+
});
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
const cookieHeader = request.headers.get('Cookie');
|
| 61 |
+
|
| 62 |
+
// Parse the cookie's value (returns an object or null if no cookie exists)
|
| 63 |
+
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
|
| 64 |
+
const providerSettings: Record<string, IProviderSetting> = JSON.parse(
|
| 65 |
+
parseCookies(cookieHeader || '').providers || '{}',
|
| 66 |
+
);
|
| 67 |
+
|
| 68 |
+
if (streamOutput) {
|
| 69 |
+
try {
|
| 70 |
+
const result = await streamText({
|
| 71 |
+
options: {
|
| 72 |
+
system,
|
| 73 |
+
},
|
| 74 |
+
messages: [
|
| 75 |
+
{
|
| 76 |
+
role: 'user',
|
| 77 |
+
content: `${message}`,
|
| 78 |
+
},
|
| 79 |
+
],
|
| 80 |
+
env: context.cloudflare.env,
|
| 81 |
+
apiKeys,
|
| 82 |
+
providerSettings,
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
return new Response(result.textStream, {
|
| 86 |
+
status: 200,
|
| 87 |
+
headers: {
|
| 88 |
+
'Content-Type': 'text/plain; charset=utf-8',
|
| 89 |
+
},
|
| 90 |
+
});
|
| 91 |
+
} catch (error: unknown) {
|
| 92 |
+
console.log(error);
|
| 93 |
+
|
| 94 |
+
if (error instanceof Error && error.message?.includes('API key')) {
|
| 95 |
+
throw new Response('Invalid or missing API key', {
|
| 96 |
+
status: 401,
|
| 97 |
+
statusText: 'Unauthorized',
|
| 98 |
+
});
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
throw new Response(null, {
|
| 102 |
+
status: 500,
|
| 103 |
+
statusText: 'Internal Server Error',
|
| 104 |
+
});
|
| 105 |
+
}
|
| 106 |
+
} else {
|
| 107 |
+
try {
|
| 108 |
+
const MODEL_LIST = await getModelList({ apiKeys, providerSettings, serverEnv: context.cloudflare.env as any });
|
| 109 |
+
const modelDetails = MODEL_LIST.find((m) => m.name === model);
|
| 110 |
+
|
| 111 |
+
if (!modelDetails) {
|
| 112 |
+
throw new Error('Model not found');
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
|
| 116 |
+
|
| 117 |
+
const providerInfo = PROVIDER_LIST.find((p) => p.name === provider.name);
|
| 118 |
+
|
| 119 |
+
if (!providerInfo) {
|
| 120 |
+
throw new Error('Provider not found');
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
const result = await generateText({
|
| 124 |
+
system,
|
| 125 |
+
messages: [
|
| 126 |
+
{
|
| 127 |
+
role: 'user',
|
| 128 |
+
content: `${message}`,
|
| 129 |
+
},
|
| 130 |
+
],
|
| 131 |
+
model: providerInfo.getModelInstance({
|
| 132 |
+
model: modelDetails.name,
|
| 133 |
+
serverEnv: context.cloudflare.env as any,
|
| 134 |
+
apiKeys,
|
| 135 |
+
providerSettings,
|
| 136 |
+
}),
|
| 137 |
+
maxTokens: dynamicMaxTokens,
|
| 138 |
+
toolChoice: 'none',
|
| 139 |
+
});
|
| 140 |
+
|
| 141 |
+
return new Response(JSON.stringify(result), {
|
| 142 |
+
status: 200,
|
| 143 |
+
headers: {
|
| 144 |
+
'Content-Type': 'application/json',
|
| 145 |
+
},
|
| 146 |
+
});
|
| 147 |
+
} catch (error: unknown) {
|
| 148 |
+
console.log(error);
|
| 149 |
+
|
| 150 |
+
if (error instanceof Error && error.message?.includes('API key')) {
|
| 151 |
+
throw new Response('Invalid or missing API key', {
|
| 152 |
+
status: 401,
|
| 153 |
+
statusText: 'Unauthorized',
|
| 154 |
+
});
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
throw new Response(null, {
|
| 158 |
+
status: 500,
|
| 159 |
+
statusText: 'Internal Server Error',
|
| 160 |
+
});
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
}
|
app/utils/selectStarterTemplate.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ignore from 'ignore';
|
| 2 |
+
import type { ProviderInfo } from '~/types/model';
|
| 3 |
+
import type { Template } from '~/types/template';
|
| 4 |
+
import { STARTER_TEMPLATES } from './constants';
|
| 5 |
+
|
| 6 |
+
const starterTemplateSelectionPrompt = (templates: Template[]) => `
|
| 7 |
+
You are an experienced developer who helps people choose the best starter template for their projects.
|
| 8 |
+
|
| 9 |
+
Available templates:
|
| 10 |
+
<template>
|
| 11 |
+
<name>blank</name>
|
| 12 |
+
<description>Empty starter for simple scripts and trivial tasks that don't require a full template setup</description>
|
| 13 |
+
<tags>basic, script</tags>
|
| 14 |
+
</template>
|
| 15 |
+
${templates
|
| 16 |
+
.map(
|
| 17 |
+
(template) => `
|
| 18 |
+
<template>
|
| 19 |
+
<name>${template.name}</name>
|
| 20 |
+
<description>${template.description}</description>
|
| 21 |
+
${template.tags ? `<tags>${template.tags.join(', ')}</tags>` : ''}
|
| 22 |
+
</template>
|
| 23 |
+
`,
|
| 24 |
+
)
|
| 25 |
+
.join('\n')}
|
| 26 |
+
|
| 27 |
+
Response Format:
|
| 28 |
+
<selection>
|
| 29 |
+
<templateName>{selected template name}</templateName>
|
| 30 |
+
<reasoning>{brief explanation for the choice}</reasoning>
|
| 31 |
+
</selection>
|
| 32 |
+
|
| 33 |
+
Examples:
|
| 34 |
+
|
| 35 |
+
<example>
|
| 36 |
+
User: I need to build a todo app
|
| 37 |
+
Response:
|
| 38 |
+
<selection>
|
| 39 |
+
<templateName>react-basic-starter</templateName>
|
| 40 |
+
<reasoning>Simple React setup perfect for building a todo application</reasoning>
|
| 41 |
+
</selection>
|
| 42 |
+
</example>
|
| 43 |
+
|
| 44 |
+
<example>
|
| 45 |
+
User: Write a script to generate numbers from 1 to 100
|
| 46 |
+
Response:
|
| 47 |
+
<selection>
|
| 48 |
+
<templateName>blank</templateName>
|
| 49 |
+
<reasoning>This is a simple script that doesn't require any template setup</reasoning>
|
| 50 |
+
</selection>
|
| 51 |
+
</example>
|
| 52 |
+
|
| 53 |
+
Instructions:
|
| 54 |
+
1. For trivial tasks and simple scripts, always recommend the blank template
|
| 55 |
+
2. For more complex projects, recommend templates from the provided list
|
| 56 |
+
3. Follow the exact XML format
|
| 57 |
+
4. Consider both technical requirements and tags
|
| 58 |
+
5. If no perfect match exists, recommend the closest option
|
| 59 |
+
|
| 60 |
+
Important: Provide only the selection tags in your response, no additional text.
|
| 61 |
+
`;
|
| 62 |
+
|
| 63 |
+
const templates: Template[] = STARTER_TEMPLATES.filter((t) => !t.name.includes('shadcn'));
|
| 64 |
+
|
| 65 |
+
const parseSelectedTemplate = (llmOutput: string): string | null => {
|
| 66 |
+
try {
|
| 67 |
+
// Extract content between <templateName> tags
|
| 68 |
+
const templateNameMatch = llmOutput.match(/<templateName>(.*?)<\/templateName>/);
|
| 69 |
+
|
| 70 |
+
if (!templateNameMatch) {
|
| 71 |
+
return null;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
return templateNameMatch[1].trim();
|
| 75 |
+
} catch (error) {
|
| 76 |
+
console.error('Error parsing template selection:', error);
|
| 77 |
+
return null;
|
| 78 |
+
}
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
export const selectStarterTemplate = async (options: { message: string; model: string; provider: ProviderInfo }) => {
|
| 82 |
+
const { message, model, provider } = options;
|
| 83 |
+
const requestBody = {
|
| 84 |
+
message,
|
| 85 |
+
model,
|
| 86 |
+
provider,
|
| 87 |
+
system: starterTemplateSelectionPrompt(templates),
|
| 88 |
+
};
|
| 89 |
+
const response = await fetch('/api/llmcall', {
|
| 90 |
+
method: 'POST',
|
| 91 |
+
body: JSON.stringify(requestBody),
|
| 92 |
+
});
|
| 93 |
+
const respJson: { text: string } = await response.json();
|
| 94 |
+
console.log(respJson);
|
| 95 |
+
|
| 96 |
+
const { text } = respJson;
|
| 97 |
+
const selectedTemplate = parseSelectedTemplate(text);
|
| 98 |
+
|
| 99 |
+
if (selectedTemplate) {
|
| 100 |
+
return selectedTemplate;
|
| 101 |
+
} else {
|
| 102 |
+
console.log('No template selected, using blank template');
|
| 103 |
+
|
| 104 |
+
return 'blank';
|
| 105 |
+
}
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
const getGitHubRepoContent = async (
|
| 109 |
+
repoName: string,
|
| 110 |
+
path: string = '',
|
| 111 |
+
): Promise<{ name: string; path: string; content: string }[]> => {
|
| 112 |
+
const baseUrl = 'https://api.github.com';
|
| 113 |
+
|
| 114 |
+
try {
|
| 115 |
+
// Fetch contents of the path
|
| 116 |
+
const response = await fetch(`${baseUrl}/repos/${repoName}/contents/${path}`, {
|
| 117 |
+
headers: {
|
| 118 |
+
Accept: 'application/vnd.github.v3+json',
|
| 119 |
+
|
| 120 |
+
// Add your GitHub token if needed
|
| 121 |
+
Authorization: 'token ' + import.meta.env.VITE_GITHUB_ACCESS_TOKEN,
|
| 122 |
+
},
|
| 123 |
+
});
|
| 124 |
+
|
| 125 |
+
if (!response.ok) {
|
| 126 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
const data: any = await response.json();
|
| 130 |
+
|
| 131 |
+
// If it's a single file, return its content
|
| 132 |
+
if (!Array.isArray(data)) {
|
| 133 |
+
if (data.type === 'file') {
|
| 134 |
+
// If it's a file, get its content
|
| 135 |
+
const content = atob(data.content); // Decode base64 content
|
| 136 |
+
return [
|
| 137 |
+
{
|
| 138 |
+
name: data.name,
|
| 139 |
+
path: data.path,
|
| 140 |
+
content,
|
| 141 |
+
},
|
| 142 |
+
];
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// Process directory contents recursively
|
| 147 |
+
const contents = await Promise.all(
|
| 148 |
+
data.map(async (item: any) => {
|
| 149 |
+
if (item.type === 'dir') {
|
| 150 |
+
// Recursively get contents of subdirectories
|
| 151 |
+
return await getGitHubRepoContent(repoName, item.path);
|
| 152 |
+
} else if (item.type === 'file') {
|
| 153 |
+
// Fetch file content
|
| 154 |
+
const fileResponse = await fetch(item.url, {
|
| 155 |
+
headers: {
|
| 156 |
+
Accept: 'application/vnd.github.v3+json',
|
| 157 |
+
Authorization: 'token ' + import.meta.env.VITE_GITHUB_ACCESS_TOKEN,
|
| 158 |
+
},
|
| 159 |
+
});
|
| 160 |
+
const fileData: any = await fileResponse.json();
|
| 161 |
+
const content = atob(fileData.content); // Decode base64 content
|
| 162 |
+
|
| 163 |
+
return [
|
| 164 |
+
{
|
| 165 |
+
name: item.name,
|
| 166 |
+
path: item.path,
|
| 167 |
+
content,
|
| 168 |
+
},
|
| 169 |
+
];
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
return [];
|
| 173 |
+
}),
|
| 174 |
+
);
|
| 175 |
+
|
| 176 |
+
// Flatten the array of contents
|
| 177 |
+
return contents.flat();
|
| 178 |
+
} catch (error) {
|
| 179 |
+
console.error('Error fetching repo contents:', error);
|
| 180 |
+
throw error;
|
| 181 |
+
}
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
export async function getTemplates(templateName: string) {
|
| 185 |
+
const template = STARTER_TEMPLATES.find((t) => t.name == templateName);
|
| 186 |
+
|
| 187 |
+
if (!template) {
|
| 188 |
+
return null;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
const githubRepo = template.githubRepo;
|
| 192 |
+
const files = await getGitHubRepoContent(githubRepo);
|
| 193 |
+
|
| 194 |
+
let filteredFiles = files;
|
| 195 |
+
|
| 196 |
+
/*
|
| 197 |
+
* ignoring common unwanted files
|
| 198 |
+
* exclude .git
|
| 199 |
+
*/
|
| 200 |
+
filteredFiles = filteredFiles.filter((x) => x.path.startsWith('.git') == false);
|
| 201 |
+
|
| 202 |
+
// exclude lock files
|
| 203 |
+
const comminLockFiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
|
| 204 |
+
filteredFiles = filteredFiles.filter((x) => comminLockFiles.includes(x.name) == false);
|
| 205 |
+
|
| 206 |
+
// exclude .bolt
|
| 207 |
+
filteredFiles = filteredFiles.filter((x) => x.path.startsWith('.bolt') == false);
|
| 208 |
+
|
| 209 |
+
// check for ignore file in .bolt folder
|
| 210 |
+
const templateIgnoreFile = files.find((x) => x.path.startsWith('.bolt') && x.name == 'ignore');
|
| 211 |
+
|
| 212 |
+
const filesToImport = {
|
| 213 |
+
files: filteredFiles,
|
| 214 |
+
ignoreFile: filteredFiles,
|
| 215 |
+
};
|
| 216 |
+
|
| 217 |
+
if (templateIgnoreFile) {
|
| 218 |
+
// redacting files specified in ignore file
|
| 219 |
+
const ignorepatterns = templateIgnoreFile.content.split('\n').map((x) => x.trim());
|
| 220 |
+
const ig = ignore().add(ignorepatterns);
|
| 221 |
+
|
| 222 |
+
// filteredFiles = filteredFiles.filter(x => !ig.ignores(x.path))
|
| 223 |
+
const ignoredFiles = filteredFiles.filter((x) => ig.ignores(x.path));
|
| 224 |
+
|
| 225 |
+
filesToImport.files = filteredFiles;
|
| 226 |
+
filesToImport.ignoreFile = ignoredFiles;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
const assistantMessage = `
|
| 230 |
+
<boltArtifact id="imported-files" title="Importing Starter Files" type="bundled">
|
| 231 |
+
${filesToImport.files
|
| 232 |
+
.map(
|
| 233 |
+
(file) =>
|
| 234 |
+
`<boltAction type="file" filePath="${file.path}">
|
| 235 |
+
${file.content}
|
| 236 |
+
</boltAction>`,
|
| 237 |
+
)
|
| 238 |
+
.join('\n')}
|
| 239 |
+
</boltArtifact>
|
| 240 |
+
`;
|
| 241 |
+
let userMessage = ``;
|
| 242 |
+
const templatePromptFile = files.filter((x) => x.path.startsWith('.bolt')).find((x) => x.name == 'prompt');
|
| 243 |
+
|
| 244 |
+
if (templatePromptFile) {
|
| 245 |
+
userMessage = `
|
| 246 |
+
TEMPLATE INSTRUCTIONS:
|
| 247 |
+
${templatePromptFile.content}
|
| 248 |
+
|
| 249 |
+
IMPORTANT: Dont Forget to install the dependencies before running the app
|
| 250 |
+
---
|
| 251 |
+
`;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
if (filesToImport.ignoreFile.length > 0) {
|
| 255 |
+
userMessage =
|
| 256 |
+
userMessage +
|
| 257 |
+
`
|
| 258 |
+
STRICT FILE ACCESS RULES - READ CAREFULLY:
|
| 259 |
+
|
| 260 |
+
The following files are READ-ONLY and must never be modified:
|
| 261 |
+
${filesToImport.ignoreFile.map((file) => `- ${file.path}`).join('\n')}
|
| 262 |
+
|
| 263 |
+
Permitted actions:
|
| 264 |
+
β Import these files as dependencies
|
| 265 |
+
β Read from these files
|
| 266 |
+
β Reference these files
|
| 267 |
+
|
| 268 |
+
Strictly forbidden actions:
|
| 269 |
+
β Modify any content within these files
|
| 270 |
+
β Delete these files
|
| 271 |
+
β Rename these files
|
| 272 |
+
β Move these files
|
| 273 |
+
β Create new versions of these files
|
| 274 |
+
β Suggest changes to these files
|
| 275 |
+
|
| 276 |
+
Any attempt to modify these protected files will result in immediate termination of the operation.
|
| 277 |
+
|
| 278 |
+
If you need to make changes to functionality, create new files instead of modifying the protected ones listed above.
|
| 279 |
+
---
|
| 280 |
+
`;
|
| 281 |
+
userMessage += `
|
| 282 |
+
Now that the Template is imported please continue with my original request
|
| 283 |
+
`;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
return {
|
| 287 |
+
assistantMessage,
|
| 288 |
+
userMessage,
|
| 289 |
+
};
|
| 290 |
+
}
|