@gzmagyari/copilot-chat-widget
v1.0.19
Published
Reusable Vue 3 chat widget component for AI agent conversations with SSE streaming support
Maintainers
Readme
Copilot Chat Widget
A fully independent, reusable chat widget component for AI agent conversations with Server-Sent Events (SSE) streaming support.
✅ Vue 2 & Vue 3 Compatible - Works with both Vue 2.6+ or Vue 3.4+ and Vuetify 2.0+ or Vuetify 3.6+. Uses Options API syntax for maximum compatibility.
Features
✅ Zero Dependencies - No Vuex/Pinia required, fully self-contained ✅ SSE Streaming - Real-time streaming with Server-Sent Events ✅ Thread Management - Create, load, and switch between conversation threads ✅ Message Editing - Edit previous messages and re-run conversations ✅ Tool Call Visualization - Display AI tool/function calls with request/response details ✅ Reasoning Display - Support for o1/o3 style reasoning summaries ✅ Configurable - API base URL via props for cross-origin support ✅ Vuetify UI - Beautiful Material Design interface
Installation
From NPM
npm install @gzmagyari/copilot-chat-widgetFrom GitHub Packages
npm install @gzmagyari/copilot-chat-widget --registry=https://npm.pkg.github.comPeer Dependencies
For Vue 3 projects:
{
"vue": "^3.4.0",
"vuetify": "^3.6.0"
}For Vue 2 projects:
{
"vue": "^2.6.0",
"vuetify": "^2.0.0"
}Usage
Basic Example
<template>
<div>
<v-btn @click="showChat = true">Open Chat</v-btn>
<ChatWidget v-model="showChat" :agent-id="123" api-base-url="" />
</div>
</template>
<script>
import { ChatWidget } from "@gzmagyari/copilot-chat-widget";
export default {
components: { ChatWidget },
data() {
return {
showChat: false
};
}
};
</script>With Custom API URL
<template>
<ChatWidget
v-model="showChat"
:agent-id="agentId"
api-base-url="https://api.example.com"
/>
</template>
<script>
import { ChatWidget } from "@gzmagyari/copilot-chat-widget";
export default {
components: { ChatWidget },
data() {
return {
showChat: true,
agentId: 456
};
}
};
</script>With Variables (e.g., API Keys)
<template>
<ChatWidget
v-model="showChat"
:agent-id="agentId"
api-base-url="https://api.example.com"
:variables="chatVariables"
/>
</template>
<script>
import { ChatWidget } from "@gzmagyari/copilot-chat-widget";
export default {
components: { ChatWidget },
data() {
return {
showChat: true,
agentId: 456,
chatVariables: {
APIKey: "your-api-key-here",
UserSettings: { theme: "dark" }
}
};
}
};
</script>With Thread Persistence (URL Parameters)
<template>
<ChatWidget
v-model="showChat"
:agent-id="agentId"
:variables="chatVariables"
:initial-thread-id="threadId"
@thread-changed="handleThreadChanged"
/>
</template>
<script>
import { ChatWidget } from "@gzmagyari/copilot-chat-widget";
export default {
components: { ChatWidget },
data() {
return {
showChat: true,
agentId: 456,
chatVariables: { APIKey: "key-123" },
// Read thread ID from URL on mount
threadId:
new URLSearchParams(window.location.search).get("thread") || null
};
},
methods: {
handleThreadChanged(newThreadId) {
// Update URL parameter when thread changes
const url = new URL(window.location.href);
url.searchParams.set("thread", newThreadId);
window.history.pushState({}, "", url);
this.threadId = newThreadId;
}
}
};
</script>Secure External Usage (Production with Authentication)
<template>
<ChatWidget
v-model="showChat"
:agent-id="agentId"
api-base-url="https://api.example.com"
:variables="chatVariables"
:initial-thread-id="threadId"
:authentication-key="userAuthKey"
@thread-changed="handleThreadChanged"
/>
</template>
<script>
import { ChatWidget } from "@gzmagyari/copilot-chat-widget";
export default {
components: { ChatWidget },
data() {
return {
showChat: true,
agentId: 456,
chatVariables: {
APIKey: "user-specific-api-key-from-backend"
},
threadId: localStorage.getItem("chatThreadId") || null,
// Generate and store unique auth key per user
userAuthKey: this.getOrCreateAuthKey()
};
},
methods: {
getOrCreateAuthKey() {
// Check if user already has an auth key
let key = localStorage.getItem("chatAuthKey");
if (!key) {
// Generate UUID v4
key = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
/[xy]/g,
function (c) {
const r = (Math.random() * 16) | 0;
const v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
}
);
localStorage.setItem("chatAuthKey", key);
}
return key;
},
handleThreadChanged(newThreadId) {
// Store thread ID for persistence
localStorage.setItem("chatThreadId", newThreadId);
this.threadId = newThreadId;
}
}
};
</script>Security Benefits:
- ✅ Each user gets a unique
authenticationKey(stored in localStorage) - ✅ Threads are linked to this key - only the creator can access them
- ✅ Thread IDs can't be hijacked - auth key is required for all operations
- ✅ Variables (like APIKey) are protected since the thread itself is secured
- ✅ Thread list only shows threads belonging to this user
Using Both Components
<template>
<div>
<!-- Full chat panel with thread management -->
<ChatWidget v-model="showPanel" :agent-id="agentId" />
<!-- Or just the chat interface component -->
<OpenAIChatWidget
:messages="messages"
:streaming-content="streamingContent"
:can-send="true"
@send-message="handleSend"
/>
</div>
</template>
<script>
import { ChatWidget, OpenAIChatWidget } from "@gzmagyari/copilot-chat-widget";
export default {
components: { ChatWidget, OpenAIChatWidget }
// ...
};
</script>Props
ChatWidget
| Prop | Type | Required | Default | Description |
| ------------------- | ---------------- | -------- | ------- | --------------------------------------------------------- |
| modelValue | Boolean | No | true | v-model for panel visibility |
| agentId | String\|Number | Yes | - | Agent ID for API calls |
| apiBaseUrl | String | No | '' | Base URL for API endpoints (empty = same origin) |
| variables | Object | No | {} | Variables to pass to the agent (e.g., API keys, settings) |
| initialThreadId | String\|Number | No | null | Thread ID to load on mount (for persistence) |
| authenticationKey | String | No | null | Unique key for secure thread access (e.g., UUID per user) |
OpenAIChatWidget
| Prop | Type | Required | Default | Description |
| ---------------------- | --------- | -------- | --------------------- | ------------------------------ |
| messages | Array | Yes | - | Array of message objects |
| streamingContent | String | No | '' | Content being streamed |
| streamingToolCalls | Array | No | [] | Tool calls being streamed |
| canSend | Boolean | No | false | Whether user can send messages |
| isWaitingForResponse | Boolean | No | false | Show loading state |
| isThinking | Boolean | No | false | Show thinking indicator |
| placeholder | String | No | 'Type a message...' | Input placeholder text |
Events
ChatWidget
| Event | Payload | Description |
| ------------------- | --------- | ----------------------------------------------------------------------- |
| update:modelValue | Boolean | Panel visibility changed (v-model) |
| thread-changed | Number | Thread selection changed (new thread created or existing thread loaded) |
OpenAIChatWidget
| Event | Payload | Description |
| ---------------- | ---------------------- | ---------------------- |
| send-message | String | User sent a message |
| edit-message | {messageId, content} | User edited a message |
| stop-streaming | - | User stopped streaming |
API Endpoints Required
Your backend must implement these endpoints:
Threads
GET /api/threads?agent_id={agentId}- List threadsPOST /api/agents/{agentId}/threads- Create threadGET /api/threads/{threadId}- Get thread detailsPOST /api/threads/{threadId}/abort- Abort streaming
Messages
GET /api/threads/{threadId}/messages- Get messagesPOST /api/threads/{threadId}/messages- Send messagePATCH /api/threads/{threadId}/messages/{messageId}- Edit messageDELETE /api/threads/{threadId}/messages/after/{messageId}- Delete after messagePOST /api/threads/{threadId}/cleanup-incomplete- Cleanup incomplete messages
Streaming
GET /api/threads/{threadId}/run/stream- SSE streaming endpoint
SSE Event Format
The streaming endpoint should send these events:
event: thinking.start
data: {}
event: content.delta
data: {"token": "Hello"}
event: tool_call.delta
data: {"id": "call_123", "name": "get_weather", "arguments_delta": "{"}
event: tool_call.complete
data: {"id": "call_123"}
event: api.request
data: {"tool_call_id": "call_123", "url": "https://api.weather.com"}
event: api.response
data: {"tool_call_id": "call_123", "status": 200, "body_snippet": {...}}
event: tool_status
data: {"tool_call_id": "call_123", "status": "completed", "status_message": "Success"}
event: assistant.message.final
data: {}
event: stream.aborted
data: {}Message Format
Messages should follow this structure:
{
id: 123,
role: "user" | "assistant" | "tool",
content: "Message text", // For user/assistant text
content_json: { // For tool calls/responses
tool_calls: [{
id: "call_123",
type: "function",
function: {
name: "get_weather",
arguments: "{\"city\": \"NYC\"}",
request: { /* HTTP request */ },
response: { /* HTTP response */ }
}
}]
},
tool_call_id: "call_123", // For tool role messages
isToolCall: true // Helper flag
}Development
Local Development
If you're working on this component locally within the CopilotClone repo:
// Use relative import
import ChatWidget from "./components/ChatWidget/ChatWidget.vue";Publishing Updates
# Navigate to the component directory
cd frontend/src/components/ChatWidget
# Bump version
npm version patch # or minor, or major
# Publish to NPM
npm publish --access public
# Or publish to GitHub Packages
npm publish --registry=https://npm.pkg.github.comUsing in Other Projects
After publishing, install in your other Vue projects:
npm install @gzmagyari/copilot-chat-widgetCustomization with Slots
The chat widget supports extensive customization through Vue slots. You can override the default rendering for any message type or indicator.
Available Slots
Message Display Slots
| Slot Name | Scope Props | Description |
| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------ |
| message-user | { message, content } | Customize user message display |
| message-assistant | { message, content } | Customize assistant text message display |
| message-tool-call | { message?, toolCall, summary, openDetails, isStreaming? } | Customize tool call display (includes function name and arguments) |
| message-tool-response | { message?, toolCall?, response, summary, openDetails } | Customize tool response display (includes HTTP response data) |
| message-tool-status | { status, toolCall } | Customize tool status updates during execution |
Indicator Slots
| Slot Name | Scope Props | Description |
| --------------------- | -------------- | -------------------------------------- |
| indicator-thinking | {} | Customize the "thinking" indicator |
| indicator-waiting | {} | Customize the waiting/typing animation |
| indicator-executing | { toolCall } | Customize the tool execution indicator |
| streaming-content | { content } | Customize streaming content display |
Customization Examples
Custom Tool Response Display
Display code diffs or structured data based on the tool that was called:
<template>
<ChatWidget v-model="showChat" :agent-id="agentId" :variables="chatVariables">
<!-- Custom tool response rendering -->
<template
#message-tool-response="{ toolCall, response, summary, openDetails }"
>
<!-- Show code diff for code editing tools -->
<div v-if="toolCall.function.name === 'edit_code'" class="code-diff-card">
<CodeDiffViewer
:old-code="response.original"
:new-code="response.modified"
:language="response.language"
/>
<v-btn small text @click="openDetails">View Raw JSON</v-btn>
</div>
<!-- Show order card for order creation -->
<div
v-else-if="toolCall.function.name === 'create_order'"
class="order-card"
>
<OrderSummaryCard :order="response.order" />
<v-chip small color="success">Order #{{ response.order.id }}</v-chip>
</div>
<!-- Show image for image generation tools -->
<div v-else-if="toolCall.function.name === 'generate_image'">
<v-img :src="response.image_url" max-width="400" class="rounded" />
<v-btn small text @click="openDetails">View Details</v-btn>
</div>
<!-- Fallback to default display -->
<div v-else class="tool-summary" @click="openDetails">
<span>Tool response: {{ summary }}</span>
<span class="click-hint">Click for details</span>
</div>
</template>
</ChatWidget>
</template>
<script>
import { ChatWidget } from "@gzmagyari/copilot-chat-widget";
import CodeDiffViewer from "./CodeDiffViewer.vue";
import OrderSummaryCard from "./OrderSummaryCard.vue";
export default {
components: { ChatWidget, CodeDiffViewer, OrderSummaryCard },
data() {
return {
showChat: true,
agentId: 123,
chatVariables: {}
};
}
};
</script>Custom Tool Call Display
Display tool calls with custom icons and formatting:
<template>
<ChatWidget :agent-id="agentId">
<template #message-tool-call="{ toolCall, openDetails, isStreaming }">
<div class="custom-tool-call" @click="openDetails">
<v-icon :color="getToolIcon(toolCall.function.name).color">
{{ getToolIcon(toolCall.function.name).icon }}
</v-icon>
<span class="tool-name">{{ toolCall.function.name }}</span>
<v-chip v-if="isStreaming" x-small color="orange">streaming</v-chip>
<span class="click-hint">Click for details</span>
</div>
</template>
</ChatWidget>
</template>
<script>
export default {
methods: {
getToolIcon(toolName) {
const icons = {
search_web: { icon: "mdi-magnify", color: "blue" },
edit_code: { icon: "mdi-code-braces", color: "purple" },
create_order: { icon: "mdi-cart", color: "green" },
send_email: { icon: "mdi-email", color: "red" }
};
return icons[toolName] || { icon: "mdi-tools", color: "grey" };
}
}
};
</script>Custom Message Display
Customize how user or assistant messages are rendered:
<template>
<ChatWidget :agent-id="agentId">
<!-- Custom user message with avatar -->
<template #message-user="{ message, content }">
<div class="custom-user-msg">
<v-avatar size="32" class="mr-2">
<img :src="userAvatar" />
</v-avatar>
<div class="message-bubble user-bubble">
{{ content }}
</div>
</div>
</template>
<!-- Custom assistant message with markdown and avatar -->
<template #message-assistant="{ content }">
<div class="custom-assistant-msg">
<v-avatar size="32" class="mr-2" color="primary">
<v-icon dark>mdi-robot</v-icon>
</v-avatar>
<div
class="message-bubble assistant-bubble"
v-html="renderMarkdown(content)"
></div>
</div>
</template>
</ChatWidget>
</template>Custom Indicators
Replace loading indicators with custom animations:
<template>
<ChatWidget :agent-id="agentId">
<!-- Custom thinking indicator -->
<template #indicator-thinking>
<div class="custom-thinking">
<v-progress-circular indeterminate color="purple" size="20" />
<span class="ml-2">AI is thinking deeply...</span>
</div>
</template>
<!-- Custom waiting indicator -->
<template #indicator-waiting>
<div class="custom-waiting">
<v-icon color="blue" class="rotating">mdi-loading</v-icon>
<span class="ml-2">Processing your request...</span>
</div>
</template>
</ChatWidget>
</template>
<style scoped>
.rotating {
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>Accessing Tool Response Data
The response prop in the message-tool-response slot contains the full JSON response from the tool:
<template #message-tool-response="{ toolCall, response }">
<div>
<!-- Access HTTP status -->
<div v-if="response.status_code">Status: {{ response.status_code }}</div>
<!-- Access response body -->
<div v-if="response.data">
<pre>{{ JSON.stringify(response.data, null, 2) }}</pre>
</div>
<!-- Conditionally render based on tool name -->
<component
:is="getComponentForTool(toolCall.function.name)"
:data="response"
/>
</div>
</template>Slot Forwarding
Slots defined on ChatWidget are automatically forwarded to the internal OpenAIChatWidget component, so you can customize the display without worrying about component hierarchy.
<!-- Both of these work the same way -->
<ChatWidget :agent-id="123">
<template #message-tool-response="{ response }">
<!-- Custom rendering -->
</template>
</ChatWidget>
<OpenAIChatWidget :messages="messages">
<template #message-tool-response="{ response }">
<!-- Custom rendering -->
</template>
</OpenAIChatWidget>Styling
The component uses Vuetify components. Make sure Vuetify is properly configured in your project:
// main.js
import { createApp } from "vue";
import { createVuetify } from "vuetify";
import "vuetify/styles";
const vuetify = createVuetify();
const app = createApp(App);
app.use(vuetify);
app.mount("#app");Browser Support
- Modern browsers (Chrome, Firefox, Safari, Edge)
- Requires native
fetch()andEventSourcesupport - Vue 3.4+ and Vuetify 3.6+
License
MIT
Contributing
This component is part of the CopilotClone project.
To contribute:
- Fork the main repository
- Make changes in
frontend/src/components/ChatWidget/ - Test locally in the CopilotClone project
- Submit a pull request
Support
For issues and questions:
- GitHub Issues: https://github.com/gzmagyari/CopilotClone/issues
- Repository: https://github.com/gzmagyari/CopilotClone
Changelog
1.1.0 (Slot Customization)
- ✅ Added customizable slots for all message types and indicators
- ✅ Support for custom tool response rendering (code diffs, cards, images, etc.)
- ✅ Custom tool call display with access to full data
- ✅ Custom loading indicators (thinking, waiting, executing)
- ✅ Slot forwarding from ChatWidget to OpenAIChatWidget
- ✅ Full Vue 2 and Vue 3 compatibility maintained
1.0.0 (Initial Release)
- ✅ Independent component with zero Vuex dependency
- ✅ SSE streaming support
- ✅ Thread management
- ✅ Message editing
- ✅ Tool call visualization
- ✅ Configurable API base URL
