Initial plugin
This commit is contained in:
384
src/airflow_wingman/templates/wingman_chat.html
Normal file
384
src/airflow_wingman/templates/wingman_chat.html
Normal file
@@ -0,0 +1,384 @@
|
||||
{% extends "appbuilder/base.html" %}
|
||||
|
||||
{% block head_meta %}
|
||||
{{ super() }}
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Banner -->
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Airflow Wingman</h3>
|
||||
</div>
|
||||
<div class="alert alert-info" style="margin: 15px;">
|
||||
<p><strong>{{ interface_messages.model_recommendation.title }}:</strong> {{ interface_messages.model_recommendation.content }}</p>
|
||||
<hr style="margin: 10px 0;">
|
||||
<p><strong>{{ interface_messages.security_note.title }}:</strong> {{ interface_messages.security_note.content }}</p>
|
||||
<hr style="margin: 10px 0;">
|
||||
<p><strong>{{ interface_messages.context_window.title }}:</strong> {{ interface_messages.context_window.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Sidebar -->
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Provider Selection</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% for provider_id, provider in models.items() %}
|
||||
<div class="provider-section mb-3">
|
||||
<h4 class="provider-name">{{ provider.name }}</h4>
|
||||
{% for model in provider.models %}
|
||||
<div class="radio model-option">
|
||||
<label class="model-label" title="{{ model.description }}">
|
||||
<input type="radio"
|
||||
name="model"
|
||||
value="{{ provider_id }}:{{ model.id }}"
|
||||
{% if model.default %}checked{% endif %}
|
||||
data-context-window="{{ model.context_window }}"
|
||||
data-provider="{{ provider_id }}"
|
||||
data-model-name="{{ model.name }}">
|
||||
{{ model.name }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Model Name Input -->
|
||||
<div class="panel-body" style="border-top: 1px solid #ddd; padding-top: 15px;">
|
||||
<div class="form-group">
|
||||
<label for="modelName">Model Name</label>
|
||||
<input type="text" class="form-control" id="modelName" placeholder="Enter model name for OpenRouter" disabled>
|
||||
<small class="form-text text-muted">Only required for OpenRouter provider</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key Input -->
|
||||
<div class="panel-body" style="border-top: 1px solid #ddd; padding-top: 15px;">
|
||||
<div class="form-group">
|
||||
<label for="api-key">API Key</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="api-key"
|
||||
placeholder="Enter API key for selected provider"
|
||||
required
|
||||
autocomplete="off">
|
||||
<small class="text-muted">Your API key will be used for the selected provider</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.provider-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.provider-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #666;
|
||||
}
|
||||
.model-option {
|
||||
margin-left: 15px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.model-option label {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Chat Window -->
|
||||
<div class="col-md-9">
|
||||
<div class="panel panel-default" style="height: calc(80vh - 250px); display: flex; flex-direction: column;">
|
||||
<div class="panel-body" style="flex-grow: 1; overflow-y: auto; padding: 15px;" id="chat-messages">
|
||||
<!-- Messages will be dynamically added here -->
|
||||
</div>
|
||||
<div class="panel-footer" style="padding: 15px; background-color: white;">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-default btn-block" type="button" id="refresh-button" title="Start a new chat">
|
||||
<i class="fa fa-refresh"></i> New Chat
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="message-input" placeholder="Type your message...">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" type="button" id="send-button">
|
||||
<i class="fa fa-paper-plane"></i> Send
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.message {
|
||||
margin-bottom: 15px;
|
||||
max-width: 80%;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.message-user {
|
||||
float: right;
|
||||
background-color: #f0f7ff;
|
||||
border: 1px solid #d1e6ff;
|
||||
border-radius: 15px 15px 0 15px;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.message-assistant {
|
||||
float: left;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 15px 15px 15px 0;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
#chat-messages::after {
|
||||
content: "";
|
||||
clear: both;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add title attributes for tooltips
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el) {
|
||||
el.title = el.getAttribute('title') || el.getAttribute('data-bs-original-title');
|
||||
});
|
||||
|
||||
// Handle model selection and model name input
|
||||
const modelNameInput = document.getElementById('modelName');
|
||||
const modelRadios = document.querySelectorAll('input[name="model"]');
|
||||
|
||||
modelRadios.forEach(function(radio) {
|
||||
radio.addEventListener('change', function() {
|
||||
const provider = this.value.split(':')[0]; // Get provider from value instead of data attribute
|
||||
const modelName = this.getAttribute('data-model-name');
|
||||
console.log('Selected provider:', provider);
|
||||
console.log('Model name:', modelName);
|
||||
|
||||
if (provider === 'openrouter') {
|
||||
console.log('Enabling model name input');
|
||||
modelNameInput.disabled = false;
|
||||
modelNameInput.value = '';
|
||||
modelNameInput.placeholder = 'Enter model name for OpenRouter';
|
||||
} else {
|
||||
console.log('Disabling model name input');
|
||||
modelNameInput.disabled = true;
|
||||
modelNameInput.value = modelName;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Set initial state based on default selection
|
||||
const defaultSelected = document.querySelector('input[name="model"]:checked');
|
||||
if (defaultSelected) {
|
||||
const provider = defaultSelected.value.split(':')[0]; // Get provider from value instead of data attribute
|
||||
const modelName = defaultSelected.getAttribute('data-model-name');
|
||||
console.log('Initial provider:', provider);
|
||||
console.log('Initial model name:', modelName);
|
||||
|
||||
if (provider === 'openrouter') {
|
||||
console.log('Initially enabling model name input');
|
||||
modelNameInput.disabled = false;
|
||||
modelNameInput.value = '';
|
||||
modelNameInput.placeholder = 'Enter model name for OpenRouter';
|
||||
} else {
|
||||
console.log('Initially disabling model name input');
|
||||
modelNameInput.disabled = true;
|
||||
modelNameInput.value = modelName;
|
||||
}
|
||||
}
|
||||
|
||||
const messageInput = document.getElementById('message-input');
|
||||
const sendButton = document.getElementById('send-button');
|
||||
const refreshButton = document.getElementById('refresh-button');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
let currentMessageDiv = null;
|
||||
let messageHistory = [];
|
||||
|
||||
function clearChat() {
|
||||
// Clear the chat messages
|
||||
chatMessages.innerHTML = '';
|
||||
// Reset message history
|
||||
messageHistory = [];
|
||||
// Clear the input field
|
||||
messageInput.value = '';
|
||||
// Enable input if it was disabled
|
||||
messageInput.disabled = false;
|
||||
sendButton.disabled = false;
|
||||
}
|
||||
|
||||
function addMessage(content, isUser) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${isUser ? 'message-user' : 'message-assistant'}`;
|
||||
messageDiv.textContent = content;
|
||||
chatMessages.appendChild(messageDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
return messageDiv;
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const message = messageInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
// Get selected model
|
||||
const selectedModel = document.querySelector('input[name="model"]:checked');
|
||||
if (!selectedModel) {
|
||||
alert('Please select a model');
|
||||
return;
|
||||
}
|
||||
|
||||
const [provider, modelId] = selectedModel.value.split(':');
|
||||
const modelName = provider === 'openrouter' ? modelNameInput.value : modelId;
|
||||
|
||||
// Clear input and add user message
|
||||
messageInput.value = '';
|
||||
addMessage(message, true);
|
||||
|
||||
try {
|
||||
// Add user message to history
|
||||
messageHistory.push({
|
||||
role: 'user',
|
||||
content: message
|
||||
});
|
||||
|
||||
// Use full message history for the request
|
||||
const messages = [...messageHistory];
|
||||
|
||||
// Create assistant message div
|
||||
currentMessageDiv = addMessage('', false);
|
||||
|
||||
// Get API key
|
||||
const apiKey = document.getElementById('api-key').value.trim();
|
||||
if (!apiKey) {
|
||||
alert('Please enter an API key');
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug log the request
|
||||
const requestData = {
|
||||
provider: provider,
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
api_key: apiKey,
|
||||
stream: true,
|
||||
temperature: 0.7
|
||||
};
|
||||
console.log('Sending request:', {...requestData, api_key: '***'});
|
||||
|
||||
// Get CSRF token
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
if (!csrfToken) {
|
||||
throw new Error('CSRF token not found. Please refresh the page.');
|
||||
}
|
||||
|
||||
// Send request
|
||||
const response = await fetch('/wingman/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider: provider,
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
api_key: apiKey,
|
||||
stream: true,
|
||||
temperature: 0.7
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to get response');
|
||||
}
|
||||
|
||||
// Handle streaming response
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let fullResponse = '';
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const content = line.slice(6);
|
||||
if (content) {
|
||||
currentMessageDiv.textContent += content;
|
||||
fullResponse += content;
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add assistant's response to history
|
||||
if (fullResponse) {
|
||||
messageHistory.push({
|
||||
role: 'assistant',
|
||||
content: fullResponse
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
currentMessageDiv.textContent = `Error: ${error.message}`;
|
||||
currentMessageDiv.style.color = 'red';
|
||||
}
|
||||
}
|
||||
|
||||
sendButton.addEventListener('click', sendMessage);
|
||||
messageInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
refreshButton.addEventListener('click', clearChat);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user