LocalAI/core/http/views/chat.html
Ettore Di Giacinto de81b42b49
Some checks failed
Explorer deployment / build-linux (push) Waiting to run
GPU tests / ubuntu-latest (1.21.x) (push) Waiting to run
generate and publish intel docker caches / generate_caches (intel/oneapi-basekit:2025.0.0-0-devel-ubuntu22.04, linux/amd64, ubuntu-latest) (push) Waiting to run
build container images / hipblas-jobs (-aio-gpu-hipblas, rocm/dev-ubuntu-22.04:6.1, hipblas, true, ubuntu:22.04, extras, latest-gpu-hipblas, latest-aio-gpu-hipblas, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, -hipblas-ffmpeg) (push) Waiting to run
build container images / hipblas-jobs (rocm/dev-ubuntu-22.04:6.1, hipblas, false, ubuntu:22.04, core, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -hipblas-core) (push) Waiting to run
build container images / hipblas-jobs (rocm/dev-ubuntu-22.04:6.1, hipblas, false, ubuntu:22.04, extras, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -hipblas) (push) Waiting to run
build container images / hipblas-jobs (rocm/dev-ubuntu-22.04:6.1, hipblas, true, ubuntu:22.04, core, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -hipblas-ffmpeg-core) (push) Waiting to run
build container images / self-hosted-jobs (-aio-gpu-intel-f16, quay.io/go-skynet/intel-oneapi-base:latest, sycl_f16, true, ubuntu:22.04, extras, latest-gpu-intel-f16, latest-aio-gpu-intel-f16, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, -sycl-f16-ffmpeg) (push) Waiting to run
build container images / self-hosted-jobs (-aio-gpu-intel-f32, quay.io/go-skynet/intel-oneapi-base:latest, sycl_f32, true, ubuntu:22.04, extras, latest-gpu-intel-f32, latest-aio-gpu-intel-f32, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, -sycl-f32-ffmpeg) (push) Waiting to run
build container images / self-hosted-jobs (-aio-gpu-nvidia-cuda-11, ubuntu:22.04, cublas, 11, 7, true, extras, latest-gpu-nvidia-cuda-11, latest-aio-gpu-nvidia-cuda-11, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, -cublas-cuda11-ffmpeg) (push) Waiting to run
build container images / self-hosted-jobs (-aio-gpu-nvidia-cuda-12, ubuntu:22.04, cublas, 12, 0, true, extras, latest-gpu-nvidia-cuda-12, latest-aio-gpu-nvidia-cuda-12, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, -cublas-cuda12-ffmpeg) (push) Waiting to run
build container images / self-hosted-jobs (quay.io/go-skynet/intel-oneapi-base:latest, sycl_f16, false, ubuntu:22.04, core, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -sycl-f16-core) (push) Waiting to run
build container images / self-hosted-jobs (quay.io/go-skynet/intel-oneapi-base:latest, sycl_f16, true, ubuntu:22.04, core, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -sycl-f16-ffmpeg-core) (push) Waiting to run
build container images / self-hosted-jobs (quay.io/go-skynet/intel-oneapi-base:latest, sycl_f32, false, ubuntu:22.04, core, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -sycl-f32-core) (push) Waiting to run
build container images / self-hosted-jobs (quay.io/go-skynet/intel-oneapi-base:latest, sycl_f32, true, ubuntu:22.04, core, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -sycl-f32-ffmpeg-core) (push) Waiting to run
build container images / self-hosted-jobs (ubuntu:22.04, , , extras, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, ) (push) Waiting to run
build container images / self-hosted-jobs (ubuntu:22.04, , true, extras, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, -ffmpeg) (push) Waiting to run
build container images / self-hosted-jobs (ubuntu:22.04, cublas, 11, 7, , extras, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -cublas-cuda11) (push) Waiting to run
build container images / self-hosted-jobs (ubuntu:22.04, cublas, 12, 0, , extras, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -cublas-cuda12) (push) Waiting to run
build container images / core-image-build (-aio-cpu, ubuntu:22.04, , true, core, latest-cpu, latest-aio-cpu, --jobs=4 --output-sync=target, linux/amd64,linux/arm64, arc-runner-set, false, auto, -ffmpeg-core) (push) Waiting to run
build container images / core-image-build (ubuntu:22.04, cublas, 11, 7, , core, --jobs=4 --output-sync=target, linux/amd64, arc-runner-set, false, false, -cublas-cuda11-core) (push) Waiting to run
build container images / core-image-build (ubuntu:22.04, cublas, 11, 7, true, core, --jobs=4 --output-sync=target, linux/amd64, arc-runner-set, false, false, -cublas-cuda11-ffmpeg-core) (push) Waiting to run
build container images / core-image-build (ubuntu:22.04, cublas, 12, 0, , core, --jobs=4 --output-sync=target, linux/amd64, arc-runner-set, false, false, -cublas-cuda12-core) (push) Waiting to run
build container images / core-image-build (ubuntu:22.04, cublas, 12, 0, true, core, --jobs=4 --output-sync=target, linux/amd64, arc-runner-set, false, false, -cublas-cuda12-ffmpeg-core) (push) Waiting to run
build container images / core-image-build (ubuntu:22.04, vulkan, true, core, latest-vulkan-ffmpeg-core, --jobs=4 --output-sync=target, linux/amd64, arc-runner-set, false, false, -vulkan-ffmpeg-core) (push) Waiting to run
build container images / gh-runner (nvcr.io/nvidia/l4t-jetpack:r36.4.0, cublas, 12, 0, true, core, latest-nvidia-l4t-arm64-core, --jobs=4 --output-sync=target, linux/arm64, ubuntu-24.04-arm, true, false, -nvidia-l4t-arm64-core) (push) Waiting to run
Security Scan / tests (push) Waiting to run
Tests extras backends / tests-transformers (push) Waiting to run
Tests extras backends / tests-rerankers (push) Waiting to run
Tests extras backends / tests-diffusers (push) Waiting to run
Tests extras backends / tests-coqui (push) Waiting to run
tests / tests-linux (1.21.x) (push) Waiting to run
tests / tests-aio-container (push) Waiting to run
tests / tests-apple (1.21.x) (push) Waiting to run
generate and publish GRPC docker caches / generate_caches (ubuntu:22.04, linux/amd64,linux/arm64, arc-runner-set) (push) Has been cancelled
feat(ui): remove api key handling and small ui adjustments (#4948)
* chore(ui): drop set api key button

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* chore(ui): shore in-progress installs in model view

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(ui): improve text to image view

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-03-05 19:37:36 +01:00

439 lines
21 KiB
HTML

<!--
Part of this page is based on the OpenAI Chatbot example by David Härer:
https://github.com/david-haerer/chatapi
MIT License Copyright (c) 2023 David Härer
Copyright (c) 2024-2025 Ettore Di Giacinto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<!doctype html>
<html lang="en">
{{template "views/partials/head" .}}
<script defer src="static/chat.js"></script>
{{ $allGalleryConfigs:=.GalleryConfig }}
{{ $model:=.Model}}
<body class="bg-slate-900 text-gray-100 flex flex-col h-screen" x-data="{ sidebarOpen: true }">
{{template "views/partials/navbar" .}}
<!-- Main container with sidebar toggle -->
<div class="flex flex-1 overflow-hidden relative">
<!-- Sidebar -->
<div
class="sidebar bg-gray-800 fixed top-16 bottom-0 left-0 w-64 transform transition-transform duration-300 ease-in-out z-30 border-r border-gray-700 overflow-y-auto"
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'">
<div class="p-4 flex justify-between items-center border-b border-gray-700">
<h2 class="text-lg font-semibold">Chat Settings</h2>
<button
@click="sidebarOpen = false"
class="text-gray-400 hover:text-white focus:outline-none">
<i class="fa-solid fa-times"></i>
</button>
</div>
<!-- Sidebar content -->
<div class="p-4 space-y-6">
<!-- Model selection - Fixed to properly select current model -->
<div class="space-y-2">
<label class="text-sm font-medium text-gray-300">Select Model</label>
<select
id="modelSelector"
class="w-full bg-gray-700 text-white border border-gray-600 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 rounded-md shadow-sm p-2 appearance-none"
onchange="window.location = this.value"
>
<option value="" disabled class="text-gray-400">Select a model</option>
{{ range .ModelsConfig }}
{{ $cfg := . }}
{{ range .KnownUsecaseStrings }}
{{ if eq . "FLAG_CHAT" }}
<option
value="chat/{{$cfg.Name}}"
{{ if eq $cfg.Name $model }} selected {{end}}
class="bg-gray-700 text-white"
>
{{$cfg.Name}}
</option>
{{ end }}
{{ end }}
{{ end }}
{{ range .ModelsWithoutConfig }}
<option
value="chat/{{.}}"
{{ if eq . $model }} selected {{ end }}
class="bg-gray-700 text-white"
>
{{.}}
</option>
{{end}}
</select>
</div>
{{ if $model }}
{{ $galleryConfig:= index $allGalleryConfigs $model}}
{{ if $galleryConfig }}
<!-- Model info -->
<div class="space-y-2">
<div class="flex items-center">
{{ if $galleryConfig.Icon }}<img src="{{$galleryConfig.Icon}}" class="rounded-lg w-8 h-8 mr-2">{{end}}
<h3 class="text-md font-medium">{{ $model }}</h3>
</div>
<button data-twe-ripple-init data-twe-ripple-color="light" class="w-full text-left flex items-center px-3 py-2 text-xs rounded text-white bg-gray-700 hover:bg-gray-600 transition-colors" data-modal-target="model-info-modal" data-modal-toggle="model-info-modal">
<i class="fas fa-info-circle mr-2"></i>
Model Information
</button>
</div>
{{ end }}
{{ end }}
<div x-data="{ activeTab: 'actions' }" class="space-y-4">
<!-- Tab navigation -->
<div class="flex border-b border-gray-700">
<button
@click="activeTab = 'actions'"
:class="activeTab === 'actions' ? 'border-b-2 border-blue-500 text-white' : 'text-gray-400 hover:text-white'"
class="py-2 px-4 text-sm font-medium">
Actions
</button>
<button
@click="activeTab = 'settings'"
:class="activeTab === 'settings' ? 'border-b-2 border-blue-500 text-white' : 'text-gray-400 hover:text-white'"
class="py-2 px-4 text-sm font-medium">
Settings
</button>
</div>
<!-- Actions tab -->
<div x-show="activeTab === 'actions'" class="space-y-3">
<button
@click="$store.chat.clear()"
id="clear"
title="Clear chat history"
class="w-full flex items-center px-3 py-2 text-sm rounded text-white bg-gray-700 hover:bg-gray-600 transition-colors"
>
<i class="fa-solid fa-trash-can mr-2"></i> Clear chat
</button>
<a
href="https://localai.io/features/text-generation/"
target="_blank"
class="w-full flex items-center px-3 py-2 text-sm rounded text-white bg-gray-700 hover:bg-gray-600 transition-colors"
>
<i class="fas fa-book mr-2"></i> Documentation
</a>
<a
href="browse?term={{.Model}}"
class="w-full flex items-center px-3 py-2 text-sm rounded text-white bg-gray-700 hover:bg-gray-600 transition-colors"
>
<i class="fas fa-brain mr-2"></i> Browse Model
</a>
</div>
<!-- Settings tab -->
<div x-show="activeTab === 'settings'" x-data="{ showPromptForm: false }" class="space-y-3">
<button
@click="showPromptForm = !showPromptForm"
class="w-full flex items-center justify-between px-3 py-2 text-sm rounded text-white bg-gray-700 hover:bg-gray-600 transition-colors"
>
<span><i class="fa-solid fa-message mr-2"></i> System Prompt</span>
<i :class="showPromptForm ? 'fa-chevron-up' : 'fa-chevron-down'" class="fa-solid"></i>
</button>
<div x-show="showPromptForm" class="p-3 bg-gray-700 rounded">
<form id="system_prompt" class="flex flex-col space-y-2">
<textarea
type="text"
id="systemPrompt"
name="systemPrompt"
class="bg-gray-800 text-white border border-gray-600 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 rounded-md shadow-sm p-2 appearance-none min-h-24"
placeholder="System prompt"
x-model.lazy="$store.chat.systemPrompt"
></textarea>
<button
type="submit"
class="px-3 py-2 text-sm rounded text-white bg-blue-600 hover:bg-blue-700 transition-colors"
>
Save System Prompt
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Main chat container (shifts with sidebar) -->
<div
class="flex-1 flex flex-col transition-all duration-300 ease-in-out"
:class="sidebarOpen ? 'ml-64' : 'ml-0'">
<!-- Chat header with toggle button -->
<div class="border-b border-gray-700 p-4 flex items-center">
<!-- Sidebar toggle button moved to be the first element in the header and with clear styling -->
<button
@click="sidebarOpen = !sidebarOpen"
class="mr-4 text-gray-300 hover:text-white focus:outline-none bg-gray-800 hover:bg-gray-700 p-2 rounded"
style="min-width: 36px;"
title="Toggle settings">
<i class="fa-solid" :class="sidebarOpen ? 'fa-times' : 'fa-bars'"></i>
</button>
<div class="flex items-center">
<i class="fa-solid fa-comments mr-2"></i>
{{ if $model }}
{{ $galleryConfig:= index $allGalleryConfigs $model}}
{{ if $galleryConfig }}
{{ if $galleryConfig.Icon }}<img src="{{$galleryConfig.Icon}}" class="rounded-lg w-8 h-8 mr-2">{{end}}
{{ end }}
{{ end }}
<h1 class="text-lg font-semibold">
Chat {{ if .Model }} with {{.Model}} {{ end }}
</h1>
</div>
</div>
<!-- Chat messages area -->
<div class="flex-1 p-4 overflow-auto" id="chat" x-data="{history: $store.chat.history}">
<p id="usage" x-show="history.length === 0" class="text-gray-300">
Start chatting with the AI by typing a prompt in the input field below and pressing Enter.
For models that support images, you can upload an image by clicking the paperclip
<i class="fa-solid fa-paperclip"></i> icon.
</p>
<div id="messages" class="max-w-3xl mx-auto">
<template x-for="message in history">
<div :class="message.role === 'user' ? 'flex items-start space-x-2 my-2 justify-end' : 'flex items-start space-x-2 my-2'">
{{ if .Model }}
{{ $galleryConfig:= index $allGalleryConfigs .Model}}
<template x-if="message.role === 'user'">
<div class="flex items-center space-x-2">
<div class="flex flex-col flex-1 items-end">
<span class="text-xs font-semibold text-gray-400">You</span>
<div class="p-2 flex-1 rounded bg-gray-700 text-white" x-html="message.html"></div>
<template x-if="message.image">
<img :src="message.image" alt="Image" class="rounded-lg mt-2 max-w-xs">
</template>
</div>
</div>
</template>
<template x-if="message.role != 'user'">
<div class="flex items-center space-x-2">
{{ if $galleryConfig }}
{{ if $galleryConfig.Icon }}<img src="{{$galleryConfig.Icon}}" class="rounded-lg mt-2 max-w-8 max-h-8">{{end}}
{{ end }}
<div class="flex flex-col flex-1">
<span class="text-xs font-semibold text-gray-400">{{if .Model}}{{.Model}}{{else}}Assistant{{end}}</span>
<div class="flex-1 text-white flex items-center space-x-2">
<div x-html="message.html"></div>
<button @click="copyToClipboard(message.html)" title="Copy to clipboard" class="text-gray-400 hover:text-gray-100">
<i class="fa-solid fa-copy"></i>
</button>
</div>
<template x-if="message.image">
<img :src="message.image" alt="Image" class="rounded-lg mt-2 max-w-xs">
</template>
</div>
</div>
</template>
{{ else }}
<i
class="fa-solid h-8 w-8"
:class="message.role === 'user' ? 'fa-user' : 'fa-robot'"
></i>
{{ end }}
</div>
</template>
</div>
</div>
<!-- Chat Input -->
<div class="p-4 border-t border-gray-700" x-data="{ inputValue: '', shiftPressed: false, fileName: '', isLoading: false }">
<form id="prompt" action="chat/{{.Model}}" method="get" @submit.prevent="submitPrompt" class="max-w-3xl mx-auto">
<div class="relative w-full bg-gray-800 rounded-xl shadow-md">
<textarea
id="input"
name="input"
x-model="inputValue"
placeholder="Send a message..."
class="p-4 pr-16 w-full bg-gray-800 text-gray-100 placeholder-gray-400 focus:outline-none resize-none border-0 rounded-xl transition-colors duration-200"
required
@keydown.shift="shiftPressed = true"
@keyup.shift="shiftPressed = false"
@keydown.enter="if (!shiftPressed) { submitPrompt($event); }"
rows="3"
style="box-shadow: 0 0 0 1px rgba(75, 85, 99, 0.4) inset;"
></textarea>
<span x-text="fileName" id="fileName" class="absolute right-16 top-4 text-gray-400 text-sm mr-2"></span>
<button
type="button"
onclick="document.getElementById('input_image').click()"
class="fa-solid fa-paperclip text-gray-400 absolute right-12 top-4 text-lg p-2 hover:text-blue-400 transition-colors duration-200"
title="Attach an image"
></button>
<!-- Send button and loader in the same position -->
<div class="absolute right-3 top-4">
<!-- Loader (hidden by default) -->
<div id="loader" class="text-lg p-2" style="display: none;">
<svg class="animate-spin h-5 w-5 text-blue-500" 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>
</div>
<!-- Send button -->
<button
id="send-button"
type="submit"
class="text-lg p-2 text-gray-400 hover:text-blue-400 transition-colors duration-200"
title="Send message"
>
<i class="fa-solid fa-paper-plane"></i>
</button>
</div>
</div>
</form>
<input id="chat-model" type="hidden" value="{{.Model}}">
<input
id="input_image"
type="file"
style="display: none;"
@change="fileName = $event.target.files[0].name"
/>
</div>
</form>
</div>
</div>
</div>
<!-- Modal moved outside of sidebar to appear in center of page -->
{{ if $model }}
{{ $galleryConfig:= index $allGalleryConfigs $model}}
{{ if $galleryConfig }}
<div id="model-info-modal" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-2xl max-h-full">
<div class="relative p-4 w-full max-w-2xl max-h-full bg-white rounded-lg shadow dark:bg-gray-700">
<!-- Header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">{{ $model }}</h3>
<button class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide="model-info-modal">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Body -->
<div class="p-4 md:p-5 space-y-4">
<div class="flex justify-center items-center">
{{ if $galleryConfig.Icon }}<img class="lazy rounded-t-lg max-h-48 max-w-96 object-cover mt-3 entered loaded" src="{{$galleryConfig.Icon}}" loading="lazy"/>{{end}}
</div>
<p class="text-base leading-relaxed text-gray-500 dark:text-gray-400">{{ $galleryConfig.Description }}</p>
<hr>
<p class="text-sm font-semibold text-gray-900 dark:text-white">Links</p>
<ul>
{{range $galleryConfig.URLs}}
<li><a href="{{ . }}" target="_blank">{{ . }}</a></li>
{{end}}
</ul>
</div>
<!-- Footer -->
<div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
<button data-modal-hide="model-info-modal" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
Close
</button>
</div>
</div>
</div>
</div>
{{ end }}
{{ end }}
<!-- Alpine store initialization -->
<script>
document.addEventListener("alpine:init", () => {
Alpine.store("chat", {
history: [],
languages: [undefined],
systemPrompt: "",
clear() {
this.history.length = 0;
},
add(role, content, image) {
const N = this.history.length - 1;
if (this.history.length && this.history[N].role === role) {
this.history[N].content += content;
this.history[N].html = DOMPurify.sanitize(
marked.parse(this.history[N].content)
);
} else {
let c = "";
const lines = content.split("\n");
lines.forEach((line) => {
c += DOMPurify.sanitize(marked.parse(line));
});
this.history.push({ role, content, html: c, image });
}
document.getElementById('messages').scrollIntoView(false);
const parser = new DOMParser();
const html = parser.parseFromString(
this.history[this.history.length - 1].html,
"text/html"
);
const code = html.querySelectorAll("pre code");
if (!code.length) return;
code.forEach((el) => {
const language = el.className.split("language-")[1];
if (this.languages.includes(language)) return;
const script = document.createElement("script");
script.src = `https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/languages/${language}.min.js`;
document.head.appendChild(script);
this.languages.push(language);
});
},
messages() {
return this.history.map((message) => ({
role: message.role,
content: message.content,
image: message.image,
}));
},
});
window.copyToClipboard = (content) => {
const tempElement = document.createElement('div');
tempElement.innerHTML = content;
const text = tempElement.textContent || tempElement.innerText;
navigator.clipboard.writeText(text).then(() => {
alert('Copied to clipboard!');
}).catch(err => {
console.error('Failed to copy: ', err);
});
};
});
</script>
</body>
</html>