|
import ignore from 'ignore'; |
|
import type { ProviderInfo } from '~/types/model'; |
|
import type { Template } from '~/types/template'; |
|
import { STARTER_TEMPLATES } from './constants'; |
|
import Cookies from 'js-cookie'; |
|
|
|
const starterTemplateSelectionPrompt = (templates: Template[]) => ` |
|
You are an experienced developer who helps people choose the best starter template for their projects. |
|
|
|
Available templates: |
|
<template> |
|
<name>blank</name> |
|
<description>Empty starter for simple scripts and trivial tasks that don't require a full template setup</description> |
|
<tags>basic, script</tags> |
|
</template> |
|
${templates |
|
.map( |
|
(template) => ` |
|
<template> |
|
<name>${template.name}</name> |
|
<description>${template.description}</description> |
|
${template.tags ? `<tags>${template.tags.join(', ')}</tags>` : ''} |
|
</template> |
|
`, |
|
) |
|
.join('\n')} |
|
|
|
Response Format: |
|
<selection> |
|
<templateName>{selected template name}</templateName> |
|
<title>{a proper title for the project}</title> |
|
</selection> |
|
|
|
Examples: |
|
|
|
<example> |
|
User: I need to build a todo app |
|
Response: |
|
<selection> |
|
<templateName>react-basic-starter</templateName> |
|
<title>Simple React todo application</title> |
|
</selection> |
|
</example> |
|
|
|
<example> |
|
User: Write a script to generate numbers from 1 to 100 |
|
Response: |
|
<selection> |
|
<templateName>blank</templateName> |
|
<title>script to generate numbers from 1 to 100</title> |
|
</selection> |
|
</example> |
|
|
|
Instructions: |
|
1. For trivial tasks and simple scripts, always recommend the blank template |
|
2. For more complex projects, recommend templates from the provided list |
|
3. Follow the exact XML format |
|
4. Consider both technical requirements and tags |
|
5. If no perfect match exists, recommend the closest option |
|
|
|
Important: Provide only the selection tags in your response, no additional text. |
|
MOST IMPORTANT: YOU DONT HAVE TIME TO THINK JUST START RESPONDING BASED ON HUNCH |
|
`; |
|
|
|
const templates: Template[] = STARTER_TEMPLATES.filter((t) => !t.name.includes('shadcn')); |
|
|
|
const parseSelectedTemplate = (llmOutput: string): { template: string; title: string } | null => { |
|
try { |
|
|
|
const templateNameMatch = llmOutput.match(/<templateName>(.*?)<\/templateName>/); |
|
const titleMatch = llmOutput.match(/<title>(.*?)<\/title>/); |
|
|
|
if (!templateNameMatch) { |
|
return null; |
|
} |
|
|
|
return { template: templateNameMatch[1].trim(), title: titleMatch?.[1].trim() || 'Untitled Project' }; |
|
} catch (error) { |
|
console.error('Error parsing template selection:', error); |
|
return null; |
|
} |
|
}; |
|
|
|
export const selectStarterTemplate = async (options: { message: string; model: string; provider: ProviderInfo }) => { |
|
const { message, model, provider } = options; |
|
const requestBody = { |
|
message, |
|
model, |
|
provider, |
|
system: starterTemplateSelectionPrompt(templates), |
|
}; |
|
const response = await fetch('/api/llmcall', { |
|
method: 'POST', |
|
body: JSON.stringify(requestBody), |
|
}); |
|
const respJson: { text: string } = await response.json(); |
|
console.log(respJson); |
|
|
|
const { text } = respJson; |
|
const selectedTemplate = parseSelectedTemplate(text); |
|
|
|
if (selectedTemplate) { |
|
return selectedTemplate; |
|
} else { |
|
console.log('No template selected, using blank template'); |
|
|
|
return { |
|
template: 'blank', |
|
title: '', |
|
}; |
|
} |
|
}; |
|
|
|
const getGitHubRepoContent = async ( |
|
repoName: string, |
|
path: string = '', |
|
): Promise<{ name: string; path: string; content: string }[]> => { |
|
const baseUrl = 'https://api.github.com'; |
|
|
|
try { |
|
const token = Cookies.get('githubToken') || import.meta.env.VITE_GITHUB_ACCESS_TOKEN; |
|
|
|
const headers: HeadersInit = { |
|
Accept: 'application/vnd.github.v3+json', |
|
}; |
|
|
|
|
|
if (token) { |
|
headers.Authorization = 'token ' + token; |
|
} |
|
|
|
|
|
const response = await fetch(`${baseUrl}/repos/${repoName}/contents/${path}`, { |
|
headers, |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
} |
|
|
|
const data: any = await response.json(); |
|
|
|
|
|
if (!Array.isArray(data)) { |
|
if (data.type === 'file') { |
|
|
|
const content = atob(data.content); |
|
return [ |
|
{ |
|
name: data.name, |
|
path: data.path, |
|
content, |
|
}, |
|
]; |
|
} |
|
} |
|
|
|
|
|
const contents = await Promise.all( |
|
data.map(async (item: any) => { |
|
if (item.type === 'dir') { |
|
|
|
return await getGitHubRepoContent(repoName, item.path); |
|
} else if (item.type === 'file') { |
|
|
|
const fileResponse = await fetch(item.url, { |
|
headers, |
|
}); |
|
const fileData: any = await fileResponse.json(); |
|
const content = atob(fileData.content); |
|
|
|
return [ |
|
{ |
|
name: item.name, |
|
path: item.path, |
|
content, |
|
}, |
|
]; |
|
} |
|
|
|
return []; |
|
}), |
|
); |
|
|
|
|
|
return contents.flat(); |
|
} catch (error) { |
|
console.error('Error fetching repo contents:', error); |
|
throw error; |
|
} |
|
}; |
|
|
|
export async function getTemplates(templateName: string, title?: string) { |
|
const template = STARTER_TEMPLATES.find((t) => t.name == templateName); |
|
|
|
if (!template) { |
|
return null; |
|
} |
|
|
|
const githubRepo = template.githubRepo; |
|
const files = await getGitHubRepoContent(githubRepo); |
|
|
|
let filteredFiles = files; |
|
|
|
|
|
|
|
|
|
|
|
filteredFiles = filteredFiles.filter((x) => x.path.startsWith('.git') == false); |
|
|
|
|
|
const comminLockFiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']; |
|
filteredFiles = filteredFiles.filter((x) => comminLockFiles.includes(x.name) == false); |
|
|
|
|
|
filteredFiles = filteredFiles.filter((x) => x.path.startsWith('.bolt') == false); |
|
|
|
|
|
const templateIgnoreFile = files.find((x) => x.path.startsWith('.bolt') && x.name == 'ignore'); |
|
|
|
const filesToImport = { |
|
files: filteredFiles, |
|
ignoreFile: [] as typeof filteredFiles, |
|
}; |
|
|
|
if (templateIgnoreFile) { |
|
|
|
const ignorepatterns = templateIgnoreFile.content.split('\n').map((x) => x.trim()); |
|
const ig = ignore().add(ignorepatterns); |
|
|
|
|
|
const ignoredFiles = filteredFiles.filter((x) => ig.ignores(x.path)); |
|
|
|
filesToImport.files = filteredFiles; |
|
filesToImport.ignoreFile = ignoredFiles; |
|
} |
|
|
|
const assistantMessage = ` |
|
<boltArtifact id="imported-files" title="${title || 'Importing Starter Files'}" type="bundled"> |
|
${filesToImport.files |
|
.map( |
|
(file) => |
|
`<boltAction type="file" filePath="${file.path}"> |
|
${file.content} |
|
</boltAction>`, |
|
) |
|
.join('\n')} |
|
</boltArtifact> |
|
`; |
|
let userMessage = ``; |
|
const templatePromptFile = files.filter((x) => x.path.startsWith('.bolt')).find((x) => x.name == 'prompt'); |
|
|
|
if (templatePromptFile) { |
|
userMessage = ` |
|
TEMPLATE INSTRUCTIONS: |
|
${templatePromptFile.content} |
|
|
|
IMPORTANT: Dont Forget to install the dependencies before running the app |
|
--- |
|
`; |
|
} |
|
|
|
if (filesToImport.ignoreFile.length > 0) { |
|
userMessage = |
|
userMessage + |
|
` |
|
STRICT FILE ACCESS RULES - READ CAREFULLY: |
|
|
|
The following files are READ-ONLY and must never be modified: |
|
${filesToImport.ignoreFile.map((file) => `- ${file.path}`).join('\n')} |
|
|
|
Permitted actions: |
|
β Import these files as dependencies |
|
β Read from these files |
|
β Reference these files |
|
|
|
Strictly forbidden actions: |
|
β Modify any content within these files |
|
β Delete these files |
|
β Rename these files |
|
β Move these files |
|
β Create new versions of these files |
|
β Suggest changes to these files |
|
|
|
Any attempt to modify these protected files will result in immediate termination of the operation. |
|
|
|
If you need to make changes to functionality, create new files instead of modifying the protected ones listed above. |
|
--- |
|
`; |
|
} |
|
|
|
userMessage += ` |
|
--- |
|
template import is done, and you can now use the imported files, |
|
edit only the files that need to be changed, and you can create new files as needed. |
|
NO NOT EDIT/WRITE ANY FILES THAT ALREADY EXIST IN THE PROJECT AND DOES NOT NEED TO BE MODIFIED |
|
--- |
|
Now that the Template is imported please continue with my original request |
|
`; |
|
|
|
return { |
|
assistantMessage, |
|
userMessage, |
|
}; |
|
} |
|
|