pvanand/code-chat-api
0
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>Agent Chat Interface</title>7 <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>8 <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">9 <link href="https://cdn.jsdelivr.net/npm/@tailwindcss/typography/dist/typography.min.css" rel="stylesheet">10 <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>11 <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>12 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">13 <style>14 .chat-message {15 transition: opacity 0.3s ease, transform 0.3s ease;16 }17 .chat-message-enter {18 opacity: 0;19 transform: translateY(10px);20 }21 .chat-message-enter-active {22 opacity: 1;23 transform: translateY(0);24 }25 .tool-section {26 transition: max-height 0.3s ease, opacity 0.3s ease;27 }28 .tool-section.collapsed {29 max-height: 0;30 opacity: 0;31 overflow: hidden;32 }33 .tool-section.expanded {34 max-height: 1000px;35 opacity: 1;36 }37 pre code {38 display: block;39 background: #f5f5f5;40 padding: 1rem;41 border-radius: 0.5rem;42 overflow-x: auto;43 }44 </style>45</head>46<body class="bg-gray-50 font-inter">47 <div id="app" class="min-h-screen flex flex-col">48 <div class="max-w-4xl mx-auto p-4 flex-1 flex flex-col w-full">49 <!-- Chat Messages -->50 <div class="bg-white rounded-lg shadow-lg p-6 mb-4 flex-1 overflow-y-auto w-full">51 <div v-for="(message, index) in messages" :key="index" class="chat-message mb-4">52 <!-- User Message -->53 <div v-if="message.role === 'user'" class="flex justify-end">54 <div class="bg-blue-500 text-white rounded-lg py-2 px-4 max-w-[80%] shadow-sm">55 {{ message.content }}56 </div>57 </div>58 59 <!-- Assistant Message -->60 <div v-else class="flex flex-col space-y-2">61 <!-- Regular Message -->62 <div v-if="message.content" class="bg-gray-100 rounded-lg py-2 px-4 max-w-[80%] shadow-sm prose">63 <div v-html="formatMarkdown(message.content)"></div>64 </div>65 66 <!-- Tool Execution -->67 <div v-if="message.tool_data" class="border border-gray-200 rounded-lg p-4 max-w-[80%] shadow-sm">68 <div class="flex items-center justify-between cursor-pointer" 69 @click="toggleTool(index)">70 <h3 class="font-semibold text-gray-700">71 Tool: {{ message.tool_data.tool }}72 </h3>73 <span class="text-gray-500">74 {{ isToolExpanded(index) ? '▼' : '▶' }}75 </span>76 </div>77 78 <div :class="['tool-section', isToolExpanded(index) ? 'expanded' : 'collapsed']">79 <!-- Tool Input -->80 <div v-if="message.tool_data.input" class="mt-2">81 <div class="text-sm text-gray-600">Input:</div>82 <pre v-if="message.tool_data.tool === 'execute_python'"><code>{{ message.tool_data.input.code }}</code></pre>83 <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>84 </div>85 86 <!-- Tool Output -->87 <div v-if="message.tool_data.output" class="mt-2">88 <div class="text-sm text-gray-600">Output:</div>89 <div v-if="message.tool_data.output.artifacts" class="space-y-4">90 <div v-for="(artifact, artifactIndex) in message.tool_data.output.artifacts" 91 :key="artifact.artifact_id"92 class="mt-4">93 <!-- Image Artifact -->94 <img v-if="artifact.artifact_type.startsWith('image/')"95 :src="artifact.imageData ? `data:${artifact.artifact_type};base64,${artifact.imageData}` : ''"96 class="max-w-full h-auto rounded shadow-sm"97 :alt="artifact.artifact_id">98 99 <!-- Plotly Artifact -->100 <div v-else-if="artifact.artifact_type === 'application/vnd.plotly.v1+json'"101 :id="'plot-' + index + '-' + artifactIndex"102 class="w-full h-96">103 </div>104 105 <!-- HTML Artifact -->106 <div v-else-if="artifact.artifact_type === 'text/html'"107 class="border rounded p-2 bg-gray-50 shadow-sm"108 v-html="artifact.content">109 </div>110 </div>111 </div>112 <pre v-if="message.tool_data.output.text" 113 class="bg-gray-50 p-2 rounded mt-1 text-sm overflow-x-auto shadow-sm">{{ message.tool_data.output.text }}</pre>114 </div>115 </div>116 </div>117 </div>118 </div>119 </div>120 121 <!-- Input Area -->122 <div class="flex space-x-4">123 <input v-model="userInput"124 @keyup.enter="sendMessage"125 type="text"126 class="flex-1 rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm"127 placeholder="Type your message...">128 <button @click="sendMessage"129 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">130 Send131 </button>132 </div>133 </div>134 </div>135 136 <script>137 const { createApp } = Vue138 139 createApp({140 data() {141 return {142 messages: [],143 userInput: '',144 expandedTools: new Set(),145 currentAssistantMessage: '',146 session_token: crypto.randomUUID()147 }148 },149 methods: {150 formatMarkdown(text) {151 return marked.parse(text)152 },153 toggleTool(index) {154 if (this.expandedTools.has(index)) {155 this.expandedTools.delete(index)156 } else {157 this.expandedTools.add(index)158 // Re-render Plotly charts when expanding, only if they haven't been rendered159 this.$nextTick(() => {160 const message = this.messages[index]161 if (message?.tool_data?.output?.artifacts) {162 const needsRendering = message.tool_data.output.artifacts.some(artifact => {163 if (artifact.artifact_type === 'application/vnd.plotly.v1+json') {164 const elementId = `plot-${index}-${message.tool_data.output.artifacts.indexOf(artifact)}`165 const element = document.getElementById(elementId)166 return element && !element._fullData167 }168 return false169 })170 if (needsRendering) {171 this.renderPlotlyCharts(index)172 }173 }174 })175 }176 },177 isToolExpanded(index) {178 return this.expandedTools.has(index)179 },180 async fetchArtifact(artifactId) {181 try {182 const response = await fetch(`https://pvanand-code-execution-files-v5.hf.space/artifact/${artifactId}`)183 if (!response.ok) throw new Error('Failed to fetch artifact')184 const data = await response.json()185 return data186 } catch (error) {187 console.error('Error fetching artifact:', error)188 return null189 }190 },191 renderPlotlyCharts(messageIndex) {192 const message = this.messages[messageIndex]193 if (!message?.tool_data?.output?.artifacts) return194 195 message.tool_data.output.artifacts.forEach((artifact, artifactIndex) => {196 if (artifact.artifact_type === 'application/vnd.plotly.v1+json' && artifact.plotData) {197 const elementId = `plot-${messageIndex}-${artifactIndex}`198 const element = document.getElementById(elementId)199 if (element) {200 // Check if a plot already exists in this element201 if (element._fullData) {202 // Update existing plot203 Plotly.react(elementId, artifact.plotData)204 } else {205 // Create new plot206 Plotly.newPlot(elementId, artifact.plotData)207 }208 }209 }210 })211 },212 async handleToolEnd(eventData) {213 // Create a new message for each tool output214 const newMessage = {215 role: 'assistant',216 tool_data: {217 tool: eventData.tool,218 output: {219 text: eventData.output.text,220 artifacts: []221 }222 }223 }224 225 if (eventData.output?.artifacts) {226 for (const artifact of eventData.output.artifacts) {227 const data = await this.fetchArtifact(artifact.artifact_id)228 229 if (artifact.artifact_type.startsWith('image/')) {230 artifact.imageData = data.data231 }232 else if (artifact.artifact_type === 'application/vnd.plotly.v1+json') {233 artifact.plotData = JSON.parse(data.data)234 }235 else if (artifact.artifact_type === 'text/html') {236 artifact.content = data.data237 }238 newMessage.tool_data.output.artifacts.push(artifact)239 }240 }241 242 this.messages.push(newMessage)243 // Expand the tool section automatically244 this.expandedTools.add(this.messages.length - 1)245 // Render Plotly charts after the DOM updates246 this.$nextTick(() => {247 this.renderPlotlyCharts(this.messages.length - 1)248 })249 },250 async sendMessage() {251 if (!this.userInput.trim()) return252 253 // Add user message254 this.messages.push({255 role: 'user',256 content: this.userInput257 })258 259 const data = {260 message: this.userInput,261 thread_id: this.session_token262 }263 264 this.userInput = ''265 this.currentAssistantMessage = ''266 267 try {268 const response = await fetch('https://pvanand-code-chat-api.hf.space/chat', {269 method: 'POST',270 headers: {271 'Content-Type': 'application/json',272 'Accept': 'text/event-stream'273 },274 body: JSON.stringify(data)275 })276 277 const reader = response.body.getReader()278 279 while (true) {280 const { done, value } = await reader.read()281 if (done) break282 283 const chunk = new TextDecoder().decode(value)284 const lines = chunk.split('\n')285 286 for (const line of lines) {287 if (!line.startsWith('data: ')) continue288 289 try {290 const eventData = JSON.parse(line.substring(6))291 292 switch (eventData.type) {293 case 'token':294 this.handleToken(eventData)295 break296 case 'tool_start':297 this.handleToolStart(eventData)298 break299 case 'tool_end':300 await this.handleToolEnd(eventData)301 break302 }303 } catch (e) {304 console.error('Error parsing event:', e)305 }306 }307 }308 } catch (error) {309 console.error('Error:', error)310 }311 },312 handleToken(eventData) {313 this.currentAssistantMessage += eventData.content314 this.updateAssistantMessage(this.currentAssistantMessage)315 },316 handleToolStart(eventData) {317 // Only create a new message if it's a new tool execution318 if (!this.messages.length || 319 this.messages[this.messages.length - 1].role !== 'assistant' ||320 this.messages[this.messages.length - 1].tool_data?.output) {321 this.messages.push({322 role: 'assistant',323 tool_data: {324 tool: eventData.tool,325 input: eventData.input326 }327 })328 }329 },330 updateAssistantMessage(content) {331 const lastMessage = this.messages[this.messages.length - 1]332 if (lastMessage?.role === 'assistant' && !lastMessage.tool_data) {333 lastMessage.content = content334 } else {335 this.messages.push({336 role: 'assistant',337 content: content338 })339 }340 }341 },342 beforeUnmount() {343 // Clean up all Plotly charts when component is destroyed344 this.messages.forEach((message, index) => {345 if (message?.tool_data?.output?.artifacts) {346 message.tool_data.output.artifacts.forEach((artifact, artifactIndex) => {347 if (artifact.artifact_type === 'application/vnd.plotly.v1+json') {348 const elementId = `plot-${index}-${artifactIndex}`349 Plotly.purge(elementId)350 }351 })352 }353 })354 }355 }).mount('#app')356 </script>357</body>358</html>