Skip to content

Instantly share code, notes, and snippets.

@lucis
Last active December 18, 2024 14:49
Show Gist options
  • Save lucis/04213e9147325b336b4d3f5400260873 to your computer and use it in GitHub Desktop.
Save lucis/04213e9147325b336b4d3f5400260873 to your computer and use it in GitHub Desktop.
<!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