Last active
December 18, 2024 14:49
-
-
Save lucis/04213e9147325b336b4d3f5400260873 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Automato Celeste</title> | |
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
<script src="https://cdn.tailwindcss.com"></script> | |
</head> | |
<body class="bg-gray-50"> | |
<div id="app" class="container mx-auto px-4 py-8"> | |
<div class="mb-4 flex items-center gap-4"> | |
<div class="flex-1"> | |
<label class="block text-sm font-medium mb-1">File:</label> | |
<select v-model="selectedFile" class="w-full p-2 border rounded"> | |
<option v-for="file in jsonFiles" :key="file" :value="file">{{ file }}</option> | |
</select> | |
</div> | |
<button @click="createNewFile" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 mt-6"> | |
New File | |
</button> | |
</div> | |
<div v-for="(block, index) in blocks" :key="index" class="mb-8"> | |
<!-- Markdown Block --> | |
<div v-if="block.type === 'markdown'" class="prose max-w-none" v-html="renderMarkdown(block.content)"> | |
</div> | |
<!-- Prompt Block --> | |
<div v-if="block.type === 'prompt' && shouldRenderBlock(index)" | |
class="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> | |
<div class="flex justify-between items-center mb-4"> | |
<h3 class="text-xl font-semibold">{{ block.title }}</h3> | |
<div class="flex gap-2"> | |
<button | |
v-if="index === lastRenderedBlockIndex && block.type === 'prompt'" | |
@click="clearBlock(index)" | |
class="px-3 py-1 text-sm text-red-600 hover:text-red-800 border border-red-600 hover:border-red-800 rounded"> | |
Clear | |
</button> | |
<button | |
@click="block.showEditInput = !block.showEditInput" | |
class="px-3 py-1 text-sm text-blue-600 hover:text-blue-800 border border-blue-600 hover:border-blue-800 rounded"> | |
Edit with AI | |
</button> | |
</div> | |
</div> | |
<!-- AI Edit Input (conditionally shown) --> | |
<div v-if="block.showEditInput" class="mb-4"> | |
<form @submit.prevent="editWithAI(block)" class="flex gap-2"> | |
<input | |
v-model="block.editRequest" | |
class="flex-1 p-2 border rounded-md focus:ring-2 focus:ring-blue-500" | |
placeholder="Describe how you want to edit this text..." | |
:disabled="block.editing" | |
> | |
<button | |
type="submit" | |
@click="block.editing ? cancelEditing(block) : undefined" | |
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 flex items-center gap-2" | |
:disabled="block.editing"> | |
<span v-if="block.editing" class="animate-spin">ó</span> | |
{{ block.editing ? 'Editing...' : 'Apply' }} | |
</button> | |
</form> | |
</div> | |
<textarea | |
v-model="block.content" | |
:placeholder="block.placeholder || ''" | |
class="w-full h-64 p-3 border rounded-md focus:ring-2 focus:ring-blue-500" | |
@input="handleInput(block)"> | |
</textarea> | |
</div> | |
<!-- Generate Files Block --> | |
<div v-if="block.type === 'generate-files' && shouldRenderBlock(index)" | |
class="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> | |
<h3 class="text-xl font-semibold mb-4">{{ block.title }}</h3> | |
<div v-if="block.files"> | |
<div v-for="file in block.files" :key="file.filename" class="mb-6 p-4 bg-gray-50 rounded-md"> | |
<h4 class="font-medium text-lg mb-3">{{ file.filename }}</h4> | |
<details class="mb-3"> | |
<summary class="cursor-pointer text-sm text-gray-600 hover:text-gray-800">View Prompt | |
</summary> | |
<textarea v-model="file.prompt" | |
class="w-full h-64 p-3 border rounded-md focus:ring-2 focus:ring-blue-500 mt-2"> | |
</textarea> | |
</details> | |
<button | |
@click="file.generating ? cancelGeneration(file) : generateFile(file)" | |
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 flex items-center"> | |
<span v-if="file.generating" class="mr-2"> | |
<svg class="animate-spin h-5 w-5" viewBox="0 0 24 24"> | |
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" | |
stroke-width="4" fill="none"></circle> | |
<path class="opacity-75" fill="currentColor" | |
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"> | |
</path> | |
</svg> | |
</span> | |
{{ file.generating ? 'Cancel Generation' : 'Create File' }} | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Next Block Button --> | |
<div v-if="shouldShowNextButton()" class="mt-4"> | |
<button @click="process()" | |
class="bg-blue-500 text-white px-6 py-2 rounded-md hover:bg-blue-600 flex items-center" | |
:disabled="processing"> | |
<span v-if="processing" class="mr-2"> | |
<svg class="animate-spin h-5 w-5" viewBox="0 0 24 24"> | |
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" | |
fill="none"></circle> | |
<path class="opacity-75" fill="currentColor" | |
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"> | |
</path> | |
</svg> | |
</span> | |
Go to {{ blocks[lastRenderedBlockIndex + 1]?.title || 'Next Step' }} | |
</button> | |
</div> | |
</div> | |
<script type="module"> | |
import { getSDK } from "https://webdraw.com/webdraw-sdk"; | |
import { marked } from 'https://esm.sh/marked'; | |
// Mock writeFile function | |
window.writeFile = (filename, content) => { | |
console.log(`Writing file: ${filename}`); | |
console.log(content); | |
}; | |
const { createApp, watch } = Vue; | |
// At the top of the script, add the mock function | |
const createFile = { | |
name: "createFile", | |
description: "Creates a file with the given name and content", | |
input_schema: { | |
type: "object", | |
properties: { | |
filename: { | |
type: "string", | |
description: "Name of the file to create" | |
}, | |
content: { | |
type: "string", | |
description: "Content of the file" | |
} | |
}, | |
required: ["filename", "content"] | |
} | |
}; | |
createApp({ | |
data() { | |
return { | |
lastRenderedBlockIndex: -1, | |
jsonFiles: [], | |
selectedFile: '', | |
sdk: null, | |
processing: false, | |
blocks: [] | |
}; | |
}, | |
async mounted() { | |
this.sdk = await getSDK(); | |
this.loadFiles(); | |
watch(() => this.selectedFile, async (newFile) => { | |
if (newFile) { | |
await this.loadContent(newFile); | |
} | |
}); | |
// Add watcher for blocks | |
watch(() => this.blocks, async (newBlocks) => { | |
if (this.selectedFile) { | |
await this.sdk.fs.write(this.selectedFile, JSON.stringify(newBlocks)); | |
} | |
}, { deep: true }); // Add deep watching to detect nested changes | |
}, | |
methods: { | |
async loadFiles() { | |
const files = await this.sdk.fs.list(); | |
this.jsonFiles = files.filter(f => f.endsWith('.json')) | |
}, | |
async loadContent(file) { | |
if (!file) return; | |
const content = await this.sdk.fs.read(file) | |
this.blocks = JSON.parse(content); | |
if (this.blocks[this.blocks.length - 1].files) { | |
this.lastRenderedBlockIndex = this.blocks.length - 1; | |
} else { | |
this.lastRenderedBlockIndex = this.blocks.findIndex(block => block.purpose && !block.content) - 1; | |
} | |
}, | |
async createNewFile() { | |
const timestamp = Date.now() | |
const newFile = `/state-${timestamp}.json` | |
await this.sdk.fs.write(newFile, JSON.stringify(this.blocks)) | |
await this.loadFiles() | |
this.selectedFile = newFile | |
}, | |
renderMarkdown(content) { | |
return marked(content); | |
}, | |
shouldRenderBlock(index) { | |
return index <= this.lastRenderedBlockIndex; | |
}, | |
shouldShowNextButton() { | |
const nextBlock = this.blocks[this.lastRenderedBlockIndex + 1]; | |
return nextBlock && nextBlock.purpose; | |
}, | |
replaceVariables(text) { | |
return text.replace(/\$\(([\w-]+)\)/g, (match, id) => { | |
const block = this.blocks.find(b => b.id === id); | |
return block ? block.content : match; | |
}); | |
}, | |
async generateFile(file) { | |
file.generating = true; | |
try { | |
// Update the prompt to be more explicit about using the tool | |
const enhancedPrompt = `${file.prompt} | |
IMPORTANT: Do not return a JSON response directly. Instead, use the createFile function to create the file. | |
Do not create output.json or response.json files. Create the actual file: ${file.filename}`; | |
const response = await this.sdk.ai.message({ | |
messages: [{ | |
role: 'user', | |
content: enhancedPrompt | |
}], | |
tools: [createFile], | |
temperature: 0.7 | |
}); | |
// Handle tool_use from the content array | |
const toolUseContent = response.content.find(item => item.type === 'tool_use'); | |
if (toolUseContent && toolUseContent.name === 'createFile') { | |
const { filename, content } = toolUseContent.input; | |
// Verify we're not creating a JSON response file | |
if (filename.endsWith('output.json') || filename.endsWith('response.json')) { | |
throw new Error('AI attempted to create a response file instead of the requested file'); | |
} | |
await this.sdk.fs.write(filename, content); | |
file.generated = true; | |
} | |
} catch (error) { | |
console.error('Error generating file:', error); | |
} finally { | |
file.generating = false; | |
} | |
}, | |
async process() { | |
const nextBlock = this.blocks[this.lastRenderedBlockIndex + 1]; | |
if (!nextBlock || this.processing) return; | |
this.processing = true; | |
try { | |
const purpose = this.replaceVariables(nextBlock.purpose); | |
if (nextBlock.type === 'generate-files') { | |
const aiPrompt = `You should return a JSON array with the strings of the filenames that should be created. | |
Return only this valid JSON array. | |
This is the context for this generation: ${purpose}`; | |
const response = await this.sdk.ai.message({ | |
messages: [{ | |
role: 'user', | |
content: aiPrompt | |
}], | |
temperature: 0.7 | |
}); | |
const filenames = JSON.parse(response.content[0].text); | |
// Create the files array with prepared prompts | |
nextBlock.files = filenames.map(filename => ({ | |
filename, | |
prompt: this.replaceVariables(nextBlock.filePromptTemplate) | |
.replace('$(file-name)', filename), | |
generating: false, | |
generated: false | |
})); | |
} else { | |
// Handle normal prompt blocks | |
const response = await this.sdk.ai.message({ | |
messages: [{ | |
role: 'user', | |
content: purpose | |
}], | |
temperature: 0.7 | |
}); | |
// For normal prompts, use the text from the first content item | |
nextBlock.content = response.content[0].text; | |
} | |
this.lastRenderedBlockIndex += 1; | |
} catch (error) { | |
console.error('Error processing block:', error); | |
} finally { | |
this.processing = false; | |
} | |
}, | |
handleInput(block) { | |
// Handle any side effects of input changes | |
console.log(`Block ${block.id} content updated`); | |
}, | |
clearBlock(index) { | |
this.blocks[index].content = ''; | |
}, | |
cancelGeneration(file) { | |
file.generating = false; | |
}, | |
async editWithAI(block) { | |
if (!block.editRequest) return; | |
block.editing = true; | |
try { | |
const response = await this.sdk.ai.message({ | |
messages: [{ | |
role: 'user', | |
content: `You're asked to edit this text: ${block.content} based on this request: ${block.editRequest}. | |
Original purpose of this step was: ${this.replaceVariables(block.purpose)} | |
Return the new value for the text` | |
}], | |
temperature: 0.7 | |
}); | |
block.content = response.content[0].text; | |
block.editRequest = ''; // Clear the input | |
block.showEditInput = false; // Hide the input | |
} catch (error) { | |
console.error('Error editing with AI:', error); | |
} finally { | |
block.editing = false; | |
} | |
} | |
} | |
}).mount('#app'); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment