<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Agent Chat Interface</title> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/@tailwindcss/typography/dist/typography.min.css" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> <style> .chat-message { transition: opacity 0.3s ease, transform 0.3s ease; } .chat-message-enter { opacity: 0; transform: translateY(10px); } .chat-message-enter-active { opacity: 1; transform: translateY(0); } .tool-section { transition: max-height 0.3s ease, opacity 0.3s ease; } .tool-section.collapsed { max-height: 0; opacity: 0; overflow: hidden; } .tool-section.expanded { max-height: 1000px; opacity: 1; } pre code { display: block; background: #f5f5f5; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; } </style> </head> <body class="bg-gray-50 font-inter"> <div id="app" class="min-h-screen flex flex-col"> <div class="max-w-4xl mx-auto p-4 flex-1 flex flex-col w-full"> <!-- Chat Messages --> <div class="bg-white rounded-lg shadow-lg p-6 mb-4 flex-1 overflow-y-auto w-full"> <div v-for="(message, index) in messages" :key="index" class="chat-message mb-4"> <!-- User Message --> <div v-if="message.role === 'user'" class="flex justify-end"> <div class="bg-blue-500 text-white rounded-lg py-2 px-4 max-w-[80%] shadow-sm"> {{ message.content }} </div> </div> <!-- Assistant Message --> <div v-else class="flex flex-col space-y-2"> <!-- Regular Message --> <div v-if="message.content" class="bg-gray-100 rounded-lg py-2 px-4 max-w-[80%] shadow-sm prose"> <div v-html="formatMarkdown(message.content)"></div> </div> <!-- Tool Execution --> <div v-if="message.tool_data" class="border border-gray-200 rounded-lg p-4 max-w-[80%] shadow-sm"> <div class="flex items-center justify-between cursor-pointer" @click="toggleTool(index)"> <h3 class="font-semibold text-gray-700"> Tool: {{ message.tool_data.tool }} </h3> <span class="text-gray-500"> {{ isToolExpanded(index) ? '▼' : '▶' }} </span> </div> <div :class="['tool-section', isToolExpanded(index) ? 'expanded' : 'collapsed']"> <!-- Tool Input --> <div v-if="message.tool_data.input" class="mt-2"> <div class="text-sm text-gray-600">Input:</div> <pre v-if="message.tool_data.tool === 'execute_python'"><code>{{ message.tool_data.input.code }}</code></pre> <pre v-else class="bg-gray-50 p-2 rounded mt-1 text-sm overflow-x-auto">{{ JSON.stringify(message.tool_data.input, null, 2) }}</pre> </div> <!-- Tool Output --> <div v-if="message.tool_data.output" class="mt-2"> <div class="text-sm text-gray-600">Output:</div> <div v-if="message.tool_data.output.artifacts" class="space-y-4"> <div v-for="(artifact, artifactIndex) in message.tool_data.output.artifacts" :key="artifact.artifact_id" class="mt-4"> <!-- Image Artifact --> <img v-if="artifact.artifact_type.startsWith('image/')" :src="artifact.imageData ? `data:${artifact.artifact_type};base64,${artifact.imageData}` : ''" class="max-w-full h-auto rounded shadow-sm" :alt="artifact.artifact_id"> <!-- Plotly Artifact --> <div v-else-if="artifact.artifact_type === 'application/vnd.plotly.v1+json'" :id="'plot-' + index + '-' + artifactIndex" class="w-full h-96"> </div> <!-- HTML Artifact --> <div v-else-if="artifact.artifact_type === 'text/html'" class="border rounded p-2 bg-gray-50 shadow-sm" v-html="artifact.content"> </div> </div> </div> <pre v-if="message.tool_data.output.text" class="bg-gray-50 p-2 rounded mt-1 text-sm overflow-x-auto shadow-sm">{{ message.tool_data.output.text }}</pre> </div> </div> </div> </div> </div> </div> <!-- Input Area --> <div class="flex space-x-4"> <input v-model="userInput" @keyup.enter="sendMessage" type="text" class="flex-1 rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm" placeholder="Type your message..."> <button @click="sendMessage" class="bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm"> Send </button> </div> </div> </div> <script> const { createApp } = Vue createApp({ data() { return { messages: [], userInput: '', expandedTools: new Set(), currentAssistantMessage: '', session_token: crypto.randomUUID() } }, methods: { formatMarkdown(text) { return marked.parse(text) }, toggleTool(index) { if (this.expandedTools.has(index)) { this.expandedTools.delete(index) } else { this.expandedTools.add(index) // Re-render Plotly charts when expanding, only if they haven't been rendered this.$nextTick(() => { const message = this.messages[index] if (message?.tool_data?.output?.artifacts) { const needsRendering = message.tool_data.output.artifacts.some(artifact => { if (artifact.artifact_type === 'application/vnd.plotly.v1+json') { const elementId = `plot-${index}-${message.tool_data.output.artifacts.indexOf(artifact)}` const element = document.getElementById(elementId) return element && !element._fullData } return false }) if (needsRendering) { this.renderPlotlyCharts(index) } } }) } }, isToolExpanded(index) { return this.expandedTools.has(index) }, async fetchArtifact(artifactId) { try { const response = await fetch(`https://pvanand-code-execution-files-v5.hf.space/artifact/${artifactId}`) if (!response.ok) throw new Error('Failed to fetch artifact') const data = await response.json() return data } catch (error) { console.error('Error fetching artifact:', error) return null } }, renderPlotlyCharts(messageIndex) { const message = this.messages[messageIndex] if (!message?.tool_data?.output?.artifacts) return message.tool_data.output.artifacts.forEach((artifact, artifactIndex) => { if (artifact.artifact_type === 'application/vnd.plotly.v1+json' && artifact.plotData) { const elementId = `plot-${messageIndex}-${artifactIndex}` const element = document.getElementById(elementId) if (element) { // Check if a plot already exists in this element if (element._fullData) { // Update existing plot Plotly.react(elementId, artifact.plotData) } else { // Create new plot Plotly.newPlot(elementId, artifact.plotData) } } } }) }, async handleToolEnd(eventData) { // Create a new message for each tool output const newMessage = { role: 'assistant', tool_data: { tool: eventData.tool, output: { text: eventData.output.text, artifacts: [] } } } if (eventData.output?.artifacts) { for (const artifact of eventData.output.artifacts) { const data = await this.fetchArtifact(artifact.artifact_id) if (artifact.artifact_type.startsWith('image/')) { artifact.imageData = data.data } else if (artifact.artifact_type === 'application/vnd.plotly.v1+json') { artifact.plotData = JSON.parse(data.data) } else if (artifact.artifact_type === 'text/html') { artifact.content = data.data } newMessage.tool_data.output.artifacts.push(artifact) } } this.messages.push(newMessage) // Expand the tool section automatically this.expandedTools.add(this.messages.length - 1) // Render Plotly charts after the DOM updates this.$nextTick(() => { this.renderPlotlyCharts(this.messages.length - 1) }) }, async sendMessage() { if (!this.userInput.trim()) return // Add user message this.messages.push({ role: 'user', content: this.userInput }) const data = { message: this.userInput, thread_id: this.session_token } this.userInput = '' this.currentAssistantMessage = '' try { const response = await fetch('https://pvanand-code-chat-api.hf.space/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }, body: JSON.stringify(data) }) const reader = response.body.getReader() while (true) { const { done, value } = await reader.read() if (done) break const chunk = new TextDecoder().decode(value) const lines = chunk.split('\n') for (const line of lines) { if (!line.startsWith('data: ')) continue try { const eventData = JSON.parse(line.substring(6)) switch (eventData.type) { case 'token': this.handleToken(eventData) break case 'tool_start': this.handleToolStart(eventData) break case 'tool_end': await this.handleToolEnd(eventData) break } } catch (e) { console.error('Error parsing event:', e) } } } } catch (error) { console.error('Error:', error) } }, handleToken(eventData) { this.currentAssistantMessage += eventData.content this.updateAssistantMessage(this.currentAssistantMessage) }, handleToolStart(eventData) { // Only create a new message if it's a new tool execution if (!this.messages.length || this.messages[this.messages.length - 1].role !== 'assistant' || this.messages[this.messages.length - 1].tool_data?.output) { this.messages.push({ role: 'assistant', tool_data: { tool: eventData.tool, input: eventData.input } }) } }, updateAssistantMessage(content) { const lastMessage = this.messages[this.messages.length - 1] if (lastMessage?.role === 'assistant' && !lastMessage.tool_data) { lastMessage.content = content } else { this.messages.push({ role: 'assistant', content: content }) } } }, beforeUnmount() { // Clean up all Plotly charts when component is destroyed this.messages.forEach((message, index) => { if (message?.tool_data?.output?.artifacts) { message.tool_data.output.artifacts.forEach((artifact, artifactIndex) => { if (artifact.artifact_type === 'application/vnd.plotly.v1+json') { const elementId = `plot-${index}-${artifactIndex}` Plotly.purge(elementId) } }) } }) } }).mount('#app') </script> </body> </html>