Initial commit: site code, layouts, content (images added separately)
Fixes duplicate placeholder cover image on the 3 newest blog posts, broken aspect-ratio classes on the albums listing page, and a non-responsive fixed sidebar on the chat page.
This commit is contained in:
498
layouts/chat/single.html
Normal file
498
layouts/chat/single.html
Normal file
@@ -0,0 +1,498 @@
|
||||
{{ define "main" }}
|
||||
<div class="bg-gray-900 min-h-screen py-8 px-4 sm:px-6 lg:px-8" x-data="chatInterface()" x-init="init()">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-extrabold text-gray-100 tracking-tight sm:text-5xl mb-4">
|
||||
Chat with Dr. Jones AI
|
||||
</h1>
|
||||
<p class="text-xl text-gray-300 max-w-2xl mx-auto">
|
||||
Get instant answers about sterile hydroponics, cannabis cultivation, and growing techniques. Drag library files into the chat to ask questions about specific documents.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Layout: Sidebar + Chat -->
|
||||
<div class="flex flex-col lg:flex-row gap-6 lg:h-[700px]">
|
||||
<!-- Left Sidebar: Library Files -->
|
||||
<div class="w-full lg:w-80 flex-shrink-0 h-64 lg:h-auto bg-gray-800 rounded-lg shadow-xl border border-gray-700 flex flex-col">
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<h2 class="text-lg font-bold text-gray-100 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Library Files
|
||||
</h2>
|
||||
<p class="text-xs text-gray-400 mt-1">Drag files into chat to reference them</p>
|
||||
</div>
|
||||
|
||||
<!-- Search/Filter -->
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<input
|
||||
type="text"
|
||||
x-model="fileSearch"
|
||||
placeholder="Search files..."
|
||||
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Files List -->
|
||||
<div class="flex-1 overflow-y-auto p-2">
|
||||
<template x-if="loadingFiles">
|
||||
<div class="text-center py-8 text-gray-400">
|
||||
<svg class="animate-spin h-6 w-6 mx-auto mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Loading files...
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loadingFiles && filteredFiles.length === 0">
|
||||
<div class="text-center py-8 text-gray-400 text-sm">
|
||||
No files found
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2" x-show="filteredFiles.length > 0">
|
||||
<template x-for="file in filteredFiles" :key="file.file">
|
||||
<div
|
||||
draggable="true"
|
||||
@dragstart="dragStart($event, file)"
|
||||
@dragend="dragEnd($event)"
|
||||
class="p-3 bg-gray-700 hover:bg-gray-600 rounded-lg cursor-move transition-colors border border-transparent hover:border-primary/30"
|
||||
:class="{ 'border-primary': isDragging && draggedFile?.file === file.file }"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-primary mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium text-gray-100 text-sm leading-tight" x-text="file.title"></div>
|
||||
<div class="text-xs text-gray-400 mt-1" x-text="file.category"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side: Chat Container -->
|
||||
<div class="flex-1 bg-gray-800 rounded-lg shadow-xl border border-gray-700 flex flex-col">
|
||||
<!-- Messages Area -->
|
||||
<div
|
||||
class="flex-1 overflow-y-auto p-6 space-y-4"
|
||||
x-ref="messagesContainer"
|
||||
@dragover.prevent="dragOver($event)"
|
||||
@drop.prevent="dropFile($event)"
|
||||
:class="{ 'bg-primary/5 border-2 border-primary border-dashed': isDragOver }"
|
||||
>
|
||||
<!-- Welcome Message -->
|
||||
<template x-if="messages.length === 0">
|
||||
<div class="text-center py-12">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/20 mb-4">
|
||||
<svg class="w-8 h-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-gray-400 text-lg mb-2">Ask me anything about sterile hydroponics!</p>
|
||||
<p class="text-gray-500 text-sm">Drag library files here to ask questions about them</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Messages -->
|
||||
<template x-for="(message, index) in messages" :key="index">
|
||||
<div class="flex" :class="message.role === 'user' ? 'justify-end' : 'justify-start'">
|
||||
<div class="max-w-[80%] rounded-lg px-4 py-3"
|
||||
:class="message.role === 'user'
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-gray-700 text-gray-100'">
|
||||
<div class="flex items-start gap-3">
|
||||
<template x-if="message.role === 'assistant'">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium mb-1" x-show="message.role === 'assistant'">Dr. Jones AI</p>
|
||||
<div class="prose prose-sm prose-invert max-w-none" x-html="formatMessage(message.content)"></div>
|
||||
<!-- Show referenced files -->
|
||||
<template x-if="message.files && message.files.length > 0">
|
||||
<div class="mt-2 pt-2 border-t border-gray-600/50">
|
||||
<div class="text-xs text-gray-400 mb-1">Referenced files:</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<template x-for="file in message.files" :key="file.file">
|
||||
<span class="text-xs px-2 py-0.5 bg-gray-600/50 rounded" x-text="file.title"></span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template x-if="message.role === 'user'">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary/30 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Loading Indicator -->
|
||||
<template x-if="loading">
|
||||
<div class="flex justify-start">
|
||||
<div class="max-w-[80%] rounded-lg px-4 py-3 bg-gray-700 text-gray-100">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex space-x-1">
|
||||
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 0ms"></div>
|
||||
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 150ms"></div>
|
||||
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 300ms"></div>
|
||||
</div>
|
||||
<span class="text-sm text-gray-400 ml-2">Dr. Jones AI is thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="border-t border-gray-700 p-4">
|
||||
<!-- Selected Files Display -->
|
||||
<div x-show="selectedFiles.length > 0" class="mb-3 pb-3 border-b border-gray-700">
|
||||
<div class="text-xs text-gray-400 mb-2">Files to include in this message:</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template x-for="file in selectedFiles" :key="file.file">
|
||||
<div class="inline-flex items-center gap-2 px-3 py-1.5 bg-primary/20 text-primary rounded-lg text-xs">
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span x-text="file.title"></span>
|
||||
<button @click="removeSelectedFile(file)" class="hover:text-primary/70">
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="sendMessage()" class="flex gap-3">
|
||||
<div class="flex-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
x-model="inputMessage"
|
||||
@keydown.enter.prevent="sendMessage()"
|
||||
:disabled="loading || rateLimited"
|
||||
placeholder="Ask about sterile hydroponics, or drag files here to ask questions about them..."
|
||||
class="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<template x-if="rateLimited">
|
||||
<p class="absolute -top-6 left-0 text-xs text-yellow-400">
|
||||
Rate limit: Please wait a moment before sending another message
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="!inputMessage.trim() || loading || rateLimited"
|
||||
class="px-6 py-3 bg-primary text-white rounded-lg font-semibold hover:bg-emerald-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
<template x-if="!loading">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||
</svg>
|
||||
<span>Send</span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
</button>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 mt-2 text-center">
|
||||
Powered by Ollama • Responses are AI-generated and should be verified
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Section -->
|
||||
<div class="mt-8 bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h2 class="text-xl font-bold text-gray-100 mb-4">About This Chat</h2>
|
||||
<div class="grid md:grid-cols-2 gap-4 text-sm text-gray-300">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-100 mb-2">What I Can Help With</h3>
|
||||
<ul class="space-y-1 list-disc list-inside text-gray-400">
|
||||
<li>Sterile hydroponic protocols</li>
|
||||
<li>Nutrient management (EC, pH)</li>
|
||||
<li>System design and setup</li>
|
||||
<li>Problem diagnosis</li>
|
||||
<li>Questions about library documents</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-100 mb-2">How to Use</h3>
|
||||
<ul class="space-y-1 list-disc list-inside text-gray-400">
|
||||
<li>Drag files from the sidebar into chat</li>
|
||||
<li>Ask questions about specific documents</li>
|
||||
<li>Up to 3 files can be referenced per message</li>
|
||||
<li>Files are processed securely on the server</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function chatInterface() {
|
||||
return {
|
||||
messages: [],
|
||||
inputMessage: '',
|
||||
loading: false,
|
||||
rateLimited: false,
|
||||
lastMessageTime: 0,
|
||||
messageCount: 0,
|
||||
rateLimitWindow: 60000,
|
||||
maxMessagesPerWindow: 10,
|
||||
// File selector state
|
||||
libraryFiles: [],
|
||||
loadingFiles: false,
|
||||
fileSearch: '',
|
||||
selectedFiles: [],
|
||||
// Drag and drop state
|
||||
isDragging: false,
|
||||
draggedFile: null,
|
||||
isDragOver: false,
|
||||
|
||||
get filteredFiles() {
|
||||
if (!this.fileSearch.trim()) {
|
||||
return this.libraryFiles;
|
||||
}
|
||||
const search = this.fileSearch.toLowerCase();
|
||||
return this.libraryFiles.filter(file =>
|
||||
file.title.toLowerCase().includes(search) ||
|
||||
file.category.toLowerCase().includes(search)
|
||||
);
|
||||
},
|
||||
|
||||
init() {
|
||||
this.apiBase = this.getApiBase();
|
||||
// Load conversation history from sessionStorage
|
||||
const saved = sessionStorage.getItem('chatHistory');
|
||||
if (saved) {
|
||||
try {
|
||||
this.messages = JSON.parse(saved);
|
||||
this.$nextTick(() => this.scrollToBottom());
|
||||
} catch (e) {
|
||||
console.error('Failed to load chat history', e);
|
||||
}
|
||||
}
|
||||
// Load library files
|
||||
this.loadLibraryFiles();
|
||||
},
|
||||
|
||||
getApiBase() {
|
||||
const host = window.location.hostname;
|
||||
const isLocal = host === 'localhost' || host === '127.0.0.1' || host.startsWith('10.');
|
||||
return isLocal ? 'http://localhost:5000' : window.location.origin.replace(/\/$/, '');
|
||||
},
|
||||
|
||||
async loadLibraryFiles() {
|
||||
this.loadingFiles = true;
|
||||
try {
|
||||
const apiUrl = `${this.apiBase}/api/library/files`;
|
||||
|
||||
const response = await fetch(apiUrl);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.libraryFiles = data.files || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load library files:', error);
|
||||
} finally {
|
||||
this.loadingFiles = false;
|
||||
}
|
||||
},
|
||||
|
||||
dragStart(event, file) {
|
||||
this.isDragging = true;
|
||||
this.draggedFile = file;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', JSON.stringify(file));
|
||||
},
|
||||
|
||||
dragEnd(event) {
|
||||
this.isDragging = false;
|
||||
this.draggedFile = null;
|
||||
},
|
||||
|
||||
dragOver(event) {
|
||||
this.isDragOver = true;
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
},
|
||||
|
||||
dropFile(event) {
|
||||
this.isDragOver = false;
|
||||
try {
|
||||
const fileData = event.dataTransfer.getData('text/plain');
|
||||
if (fileData) {
|
||||
const file = JSON.parse(fileData);
|
||||
this.addSelectedFile(file);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error handling drop:', e);
|
||||
}
|
||||
},
|
||||
|
||||
addSelectedFile(file) {
|
||||
// Limit to 3 files max
|
||||
if (this.selectedFiles.length >= 3) {
|
||||
alert('You can reference up to 3 files at a time.');
|
||||
return;
|
||||
}
|
||||
// Don't add duplicates
|
||||
if (!this.selectedFiles.find(f => f.file === file.file)) {
|
||||
this.selectedFiles.push(file);
|
||||
}
|
||||
},
|
||||
|
||||
removeSelectedFile(file) {
|
||||
this.selectedFiles = this.selectedFiles.filter(f => f.file !== file.file);
|
||||
},
|
||||
|
||||
formatMessage(text) {
|
||||
const safe = this.escapeHTML(text || '');
|
||||
return safe
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/`(.*?)`/g, '<code class="bg-gray-900 px-1 py-0.5 rounded text-primary">$1</code>');
|
||||
},
|
||||
|
||||
escapeHTML(text) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
checkRateLimit() {
|
||||
const now = Date.now();
|
||||
if (now - this.lastMessageTime > this.rateLimitWindow) {
|
||||
this.messageCount = 0;
|
||||
this.lastMessageTime = now;
|
||||
}
|
||||
if (this.messageCount >= this.maxMessagesPerWindow) {
|
||||
this.rateLimited = true;
|
||||
setTimeout(() => {
|
||||
this.rateLimited = false;
|
||||
this.messageCount = 0;
|
||||
}, this.rateLimitWindow - (now - this.lastMessageTime));
|
||||
return false;
|
||||
}
|
||||
this.messageCount++;
|
||||
this.lastMessageTime = now;
|
||||
return true;
|
||||
},
|
||||
|
||||
async sendMessage() {
|
||||
const message = this.inputMessage.trim();
|
||||
if (!message || this.loading || this.rateLimited) return;
|
||||
|
||||
if (!this.checkRateLimit()) return;
|
||||
|
||||
const safeForDisplay = this.escapeHTML(message);
|
||||
|
||||
// Store selected files for this message
|
||||
const filesForMessage = [...this.selectedFiles];
|
||||
|
||||
// Add user message
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: safeForDisplay,
|
||||
files: filesForMessage.length > 0 ? filesForMessage : undefined
|
||||
});
|
||||
|
||||
this.inputMessage = '';
|
||||
this.selectedFiles = []; // Clear selected files after sending
|
||||
this.loading = true;
|
||||
this.saveHistory();
|
||||
this.scrollToBottom();
|
||||
|
||||
try {
|
||||
const sanitizedMessage = this.sanitizeInput(message);
|
||||
const apiUrl = `${this.apiBase}/api/chat`;
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: sanitizedMessage,
|
||||
files: filesForMessage.map(f => ({ file: f.file, title: f.title })), // Only send file references
|
||||
history: this.messages.slice(-5)
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: data.response || 'Sorry, I encountered an error. Please try again.'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Chat error:', error);
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: 'Sorry, I\'m having trouble connecting right now. Please try again in a moment.'
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.saveHistory();
|
||||
this.scrollToBottom();
|
||||
}
|
||||
},
|
||||
|
||||
sanitizeInput(input) {
|
||||
return input
|
||||
.replace(/[<>]/g, '')
|
||||
.substring(0, 1000)
|
||||
.trim();
|
||||
},
|
||||
|
||||
saveHistory() {
|
||||
try {
|
||||
sessionStorage.setItem('chatHistory', JSON.stringify(this.messages));
|
||||
} catch (e) {
|
||||
console.error('Failed to save chat history', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user