pvanand's picture
Update static/chatui/index.html
3644f88 verified
<!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/[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>