Spaces:
Running
Running
<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/[email protected]/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> |