codacus commited on
Commit
4c81e15
Β·
unverified Β·
1 Parent(s): 3a36a44

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 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-sm text-bolt-elements-textSecondary">
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-4">
63
  Disclaimer: Experimental features may be unstable and are subject to change.
64
  </p>
65
-
66
- <div className="flex items-center justify-between mb-2">
67
- <span className="text-bolt-elements-textPrimary">Experimental Providers</span>
68
- <Switch className="ml-auto" checked={isLocalModel} onCheckedChange={enableLocalModels} />
 
 
 
 
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-sm text-bolt-elements-textSecondary mb-4">
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
+ }