Skip to content

Commit

Permalink
feat(ui): allow to set system prompt for chat
Browse files Browse the repository at this point in the history
Make also the models in the index clickable, and display as table

Fixes #2257

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
  • Loading branch information
mudler committed May 7, 2024
1 parent d1e3436 commit 4ca17fd
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 30 deletions.
19 changes: 8 additions & 11 deletions core/http/routes/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func RegisterUIRoutes(app *fiber.App,

// Show the Models page (all models)
app.Get("/browse", auth, func(c *fiber.Ctx) error {
term := c.Query("term")

models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)

// Get all available tags
Expand All @@ -47,6 +49,11 @@ func RegisterUIRoutes(app *fiber.App,
tags = append(tags, t)
}
sort.Strings(tags)

if term != "" {
models = gallery.GalleryModels(models).Search(term)
}

summary := fiber.Map{
"Title": "LocalAI - Models",
"Version": internal.PrintableVersion(),
Expand All @@ -72,17 +79,7 @@ func RegisterUIRoutes(app *fiber.App,

models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)

filteredModels := []*gallery.GalleryModel{}
for _, m := range models {
if strings.Contains(m.Name, form.Search) ||
strings.Contains(m.Description, form.Search) ||
strings.Contains(m.Gallery.Name, form.Search) ||
strings.Contains(strings.Join(m.Tags, ","), form.Search) {
filteredModels = append(filteredModels, m)
}
}

return c.SendString(elements.ListModels(filteredModels, installingModels))
return c.SendString(elements.ListModels(gallery.GalleryModels(models).Search(form.Search), installingModels))
})

/*
Expand Down
33 changes: 29 additions & 4 deletions core/http/static/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

function submitKey(event) {
event.preventDefault();
localStorage.setItem("key", document.getElementById("apiKey").value);
document.getElementById("apiKey").blur();
}
}

function submitSystemPrompt(event) {
event.preventDefault();
localStorage.setItem("system_prompt", document.getElementById("systemPrompt").value);
document.getElementById("systemPrompt").blur();
}

function submitPrompt(event) {
event.preventDefault();
Expand All @@ -39,12 +46,13 @@ function submitPrompt(event) {
Alpine.store("chat").add("user", input);
document.getElementById("input").value = "";
const key = localStorage.getItem("key");
const systemPrompt = localStorage.getItem("system_prompt");

promptGPT(key, input);
promptGPT(systemPrompt, key, input);
}


async function promptGPT(key, input) {
async function promptGPT(systemPrompt, key, input) {
const model = document.getElementById("chat-model").value;
// Set class "loader" to the element with "loader" id
//document.getElementById("loader").classList.add("loader");
Expand All @@ -53,6 +61,16 @@ function submitPrompt(event) {
document.getElementById("input").disabled = true;
document.getElementById('messages').scrollIntoView(false)

messages = Alpine.store("chat").messages();

// if systemPrompt isn't empty, push it at the start of messages
if (systemPrompt) {
messages.unshift({
role: "system",
content: systemPrompt
});
}

// Source: https://stackoverflow.com/a/75751803/11386095
const response = await fetch("/v1/chat/completions", {
method: "POST",
Expand All @@ -62,7 +80,7 @@ function submitPrompt(event) {
},
body: JSON.stringify({
model: model,
messages: Alpine.store("chat").messages(),
messages: messages,
stream: true,
}),
});
Expand Down Expand Up @@ -122,13 +140,20 @@ function submitPrompt(event) {
}

document.getElementById("key").addEventListener("submit", submitKey);
document.getElementById("system_prompt").addEventListener("submit", submitSystemPrompt);

document.getElementById("prompt").addEventListener("submit", submitPrompt);
document.getElementById("input").focus();

const storeKey = localStorage.getItem("key");
if (storeKey) {
document.getElementById("apiKey").value = storeKey;
}

const storesystemPrompt = localStorage.getItem("system_prompt");
if (storesystemPrompt) {
document.getElementById("systemPrompt").value = storesystemPrompt;
}

marked.setOptions({
highlight: function (code) {
Expand Down
19 changes: 18 additions & 1 deletion core/http/views/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,34 @@ <h1 class="text-lg font-semibold"> <i class="fa-solid fa-comments"></i> Chat wit
<button @click="component = 'key'" title="Update API key"
class="m-2 float-right inline-block rounded bg-primary px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 hover:shadow-primary-2 focus:bg-primary-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-primary-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong"
>Set API Key🔑</button>
<button @click="component = 'system_prompt'" title="System Prompt"
class="m-2 float-right inline-block rounded bg-primary px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 hover:shadow-primary-2 focus:bg-primary-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-primary-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong"
>Set system prompt</button>
</div>
<form x-show="component === 'key'" id="key">
<input
type="password"
id="apiKey"
name="apiKey"
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"
placeholder="OpenAI API Key"
x-model.lazy="key"
/>
<button @click="component = 'menu'" type="submit" title="Save API key">
🔒
<i class="fa-solid fa-arrow-right"></i>
</button>
</form>
<form x-show="component === 'system_prompt'" id="system_prompt">
<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"
placeholder="System prompt"
x-model.lazy="system_prompt"
></textarea>
<button @click="component = 'menu'" type="submit" title="Save Prompt">
<i class="fa-solid fa-arrow-right"></i>
</button>
</form>

Expand Down
43 changes: 30 additions & 13 deletions core/http/views/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,53 @@
<div class="header text-center py-12">
<h1 class="text-5xl font-bold text-gray-100">Welcome to <i>your</i> LocalAI instance!</h1>
<div class="mt-6">
<!-- Logo can be uncommented and updated with a valid URL -->
</div>
<p class="mt-4 text-lg">The FOSS alternative to OpenAI, Claude, ...</p>
<a href="https://localai.io" target="_blank" class="mt-4 inline-block bg-blue-500 text-white py-2 px-4 rounded-lg shadow transition duration-300 ease-in-out hover:bg-blue-700 hover:shadow-lg">
<i class="fas fa-book-reader pr-2"></i>Documentation
</a>
</div>

<div class="models mt-12">
<div class="models mt-4">
{{ if eq (len .ModelsConfig) 0 }}
<h2 class="text-center text-3xl font-semibold text-gray-100"> <i class="text-yellow-200 ml-2 fa-solid fa-triangle-exclamation animate-pulse"></i> Ouch! seems you don't have any models installed!</h2>
<p class="text-center mt-4 text-xl">..install something from the <a class="text-gray-400 hover:text-white ml-1 px-3 py-2 rounded" href="/browse">🖼️ Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-gray-400 hover:text-white ml-1 px-3 py-2 rounded"> <i class="fa-solid fa-book"></i> Getting started documentation </a></p>
{{ else }}

<h2 class="text-center text-3xl font-semibold text-gray-100">Installed models</h2>
<p class="text-center mt-4 text-xl">We have {{len .ModelsConfig}} pre-loaded models available.</p>
<ul class="mt-8 space-y-4">
<table class="table-auto mt-4 w-full text-left text-gray-200">
<thead class="text-xs text-gray-400 uppercase bg-gray-700">
<tr>
<th class="px-4 py-2"></th>
<th class="px-4 py-2">Model Name</th>
<th class="px-4 py-2">Backend</th>
<th class="px-4 py-2 float-right">Actions</th>
</tr>
</thead>
<tbody>
{{$galleryConfig:=.GalleryConfig}}
{{$noicon:="https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg"}}
{{ range .ModelsConfig }}
{{ $cfg:= index $galleryConfig .Name}}
<li class="bg-gray-800 border border-gray-700 p-4 rounded-lg">
<div class="flex justify-between items-center">

<tr class="bg-gray-800 border-b border-gray-700">
<td class="px-4 py-3">
{{ with $cfg }}
<img {{ if $cfg.Icon }}
src="{{$cfg.Icon}}"
{{ else }}
src="https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg"
src="{{$noicon}}"
{{ end }}
class="rounded-t-lg max-h-24 max-w-24 object-cover mt-3"
>

<p class="font-bold text-white flex items-center"><i class="fas fa-brain pr-2"></i>{{.Name}}</p>
{{ else}}
<img src="{{$noicon}}" class="rounded-t-lg max-h-24 max-w-24 object-cover mt-3">
{{ end }}
</td>
<td class="px-4 py-3 font-bold">
<p class="font-bold text-white flex items-center"><i class="fas fa-brain pr-2"></i><a href="/browse?term={{.Name}}">{{.Name}}</a></p>
</td>
<td class="px-4 py-3 font-bold">
{{ if .Backend }}
<!-- Badge for Backend -->
<span class="inline-block bg-blue-500 text-white py-1 px-3 rounded-full text-xs">
Expand All @@ -52,15 +68,16 @@ <h2 class="text-center text-3xl font-semibold text-gray-100">Installed models</h
auto
</span>
{{ end }}
</td>

<td class="px-4 py-3">
<button
class="float-right inline-block rounded bg-red-800 px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-red-accent-300 hover:shadow-red-2 focus:bg-red-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-red-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong"
data-twe-ripple-color="light" data-twe-ripple-init="" hx-confirm="Are you sure you wish to delete the model?" hx-post="/browse/delete/model/{{.Name}}" hx-swap="outerHTML"><i class="fa-solid fa-cancel pr-2"></i>Delete</button>
</div>
<!-- Additional details can go here -->
</li>
</td>
{{ end }}
</ul>
</tbody>
</table>
{{ end }}
</div>
</div>
Expand Down
21 changes: 20 additions & 1 deletion pkg/gallery/request.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package gallery

import "fmt"
import (
"fmt"
"strings"
)

// GalleryModel is the struct used to represent a model in the gallery returned by the endpoint.
// It is used to install the model by resolving the URL and downloading the files.
Expand Down Expand Up @@ -28,3 +31,19 @@ type GalleryModel struct {
func (m GalleryModel) ID() string {
return fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)
}

type GalleryModels []*GalleryModel

func (gm GalleryModels) Search(term string) GalleryModels {
var filteredModels GalleryModels

for _, m := range gm {
if strings.Contains(m.Name, term) ||
strings.Contains(m.Description, term) ||
strings.Contains(m.Gallery.Name, term) ||
strings.Contains(strings.Join(m.Tags, ","), term) {
filteredModels = append(filteredModels, m)
}
}
return filteredModels
}

0 comments on commit 4ca17fd

Please sign in to comment.