Spaces:
Sleeping
Sleeping
| <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> |