Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/http/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func API(application *application.Application) (*echo.Echo, error) {
routes.RegisterLocalAIRoutes(e, requestExtractor, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService(), opcache, application.TemplatesEvaluator())
routes.RegisterOpenAIRoutes(e, requestExtractor, application)
if !application.ApplicationConfig().DisableWebUI {
routes.RegisterUIAPIRoutes(e, application.ModelConfigLoader(), application.ApplicationConfig(), application.GalleryService(), opcache)
routes.RegisterUIAPIRoutes(e, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService(), opcache)
routes.RegisterUIRoutes(e, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService())
}
routes.RegisterJINARoutes(e, requestExtractor, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig())
Expand Down
37 changes: 26 additions & 11 deletions core/http/routes/ui_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import (
"github.com/mudler/LocalAI/core/gallery"
"github.com/mudler/LocalAI/core/p2p"
"github.com/mudler/LocalAI/core/services"
"github.com/mudler/LocalAI/pkg/model"
"github.com/rs/zerolog/log"
)

// RegisterUIAPIRoutes registers JSON API routes for the web UI
func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, appConfig *config.ApplicationConfig, galleryService *services.GalleryService, opcache *services.OpCache) {
func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig, galleryService *services.GalleryService, opcache *services.OpCache) {

// Operations API - Get all current operations (models + backends)
app.GET("/api/operations", func(c echo.Context) error {
Expand Down Expand Up @@ -257,17 +258,23 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, appConfig
nextPage = totalPages
}

// Calculate installed models count (models with configs + models without configs)
modelConfigs := cl.GetAllModelsConfigs()
modelsWithoutConfig, _ := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY)
installedModelsCount := len(modelConfigs) + len(modelsWithoutConfig)

return c.JSON(200, map[string]interface{}{
"models": modelsJSON,
"repositories": appConfig.Galleries,
"allTags": tags,
"processingModels": processingModelsData,
"taskTypes": taskTypes,
"availableModels": totalModels,
"currentPage": pageNum,
"totalPages": totalPages,
"prevPage": prevPage,
"nextPage": nextPage,
"models": modelsJSON,
"repositories": appConfig.Galleries,
"allTags": tags,
"processingModels": processingModelsData,
"taskTypes": taskTypes,
"availableModels": totalModels,
"installedModels": installedModelsCount,
"currentPage": pageNum,
"totalPages": totalPages,
"prevPage": prevPage,
"nextPage": nextPage,
})
})

Expand Down Expand Up @@ -551,13 +558,21 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, appConfig
nextPage = totalPages
}

// Calculate installed backends count
installedBackends, err := gallery.ListSystemBackends(appConfig.SystemState)
installedBackendsCount := 0
if err == nil {
installedBackendsCount = len(installedBackends)
}

return c.JSON(200, map[string]interface{}{
"backends": backendsJSON,
"repositories": appConfig.BackendGalleries,
"allTags": tags,
"processingBackends": processingBackendsData,
"taskTypes": taskTypes,
"availableBackends": totalBackends,
"installedBackends": installedBackendsCount,
"currentPage": pageNum,
"totalPages": totalPages,
"prevPage": prevPage,
Expand Down
186 changes: 149 additions & 37 deletions core/http/static/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ var images = [];
var audios = [];
var fileContents = [];
var currentFileNames = [];
// Track file names to data URLs for proper removal
var imageFileMap = new Map(); // fileName -> dataURL
var audioFileMap = new Map(); // fileName -> dataURL

async function extractTextFromPDF(pdfData) {
try {
Expand All @@ -197,35 +200,119 @@ async function extractTextFromPDF(pdfData) {
}
}

// Global function to handle file selection and update Alpine.js state
window.handleFileSelection = function(event, fileType) {
if (!event.target.files || !event.target.files.length) return;

// Get the Alpine.js component - find the parent div with x-data containing attachedFiles
let inputContainer = event.target.closest('[x-data*="attachedFiles"]');
if (!inputContainer && window.Alpine) {
// Fallback: find any element with attachedFiles in x-data
inputContainer = document.querySelector('[x-data*="attachedFiles"]');
}
if (!inputContainer || !window.Alpine) return;

const alpineData = Alpine.$data(inputContainer);
if (!alpineData || !alpineData.attachedFiles) return;

Array.from(event.target.files).forEach(file => {
// Check if file already exists
const exists = alpineData.attachedFiles.some(f => f.name === file.name && f.type === fileType);
if (!exists) {
alpineData.attachedFiles.push({ name: file.name, type: fileType });

// Process the file based on type
if (fileType === 'image') {
readInputImageFile(file);
} else if (fileType === 'audio') {
readInputAudioFile(file);
} else if (fileType === 'file') {
readInputFileFile(file);
}
}
});
};

// Global function to remove file from input
window.removeFileFromInput = function(fileType, fileName) {
// Remove from arrays
if (fileType === 'image') {
// Remove from images array using the mapping
const dataURL = imageFileMap.get(fileName);
if (dataURL) {
const imageIndex = images.indexOf(dataURL);
if (imageIndex !== -1) {
images.splice(imageIndex, 1);
}
imageFileMap.delete(fileName);
}
} else if (fileType === 'audio') {
// Remove from audios array using the mapping
const dataURL = audioFileMap.get(fileName);
if (dataURL) {
const audioIndex = audios.indexOf(dataURL);
if (audioIndex !== -1) {
audios.splice(audioIndex, 1);
}
audioFileMap.delete(fileName);
}
} else if (fileType === 'file') {
// Remove from fileContents and currentFileNames
const fileIndex = currentFileNames.indexOf(fileName);
if (fileIndex !== -1) {
currentFileNames.splice(fileIndex, 1);
fileContents.splice(fileIndex, 1);
}
}

// Also remove from the actual input element
const inputId = fileType === 'image' ? 'input_image' :
fileType === 'audio' ? 'input_audio' : 'input_file';
const input = document.getElementById(inputId);
if (input && input.files) {
const dt = new DataTransfer();
Array.from(input.files).forEach(file => {
if (file.name !== fileName) {
dt.items.add(file);
}
});
input.files = dt.files;
}
};

function readInputFile() {
if (!this.files || !this.files.length) return;

Array.from(this.files).forEach(file => {
const FR = new FileReader();
currentFileNames.push(file.name);
const fileExtension = file.name.split('.').pop().toLowerCase();

FR.addEventListener("load", async function(evt) {
if (fileExtension === 'pdf') {
try {
const content = await extractTextFromPDF(evt.target.result);
fileContents.push({ name: file.name, content: content });
} catch (error) {
console.error('Error processing PDF:', error);
fileContents.push({ name: file.name, content: "Error processing PDF file" });
}
} else {
// For text and markdown files
fileContents.push({ name: file.name, content: evt.target.result });
}
});
readInputFileFile(file);
});
}

function readInputFileFile(file) {
const FR = new FileReader();
currentFileNames.push(file.name);
const fileExtension = file.name.split('.').pop().toLowerCase();

FR.addEventListener("load", async function(evt) {
if (fileExtension === 'pdf') {
FR.readAsArrayBuffer(file);
try {
const content = await extractTextFromPDF(evt.target.result);
fileContents.push({ name: file.name, content: content });
} catch (error) {
console.error('Error processing PDF:', error);
fileContents.push({ name: file.name, content: "Error processing PDF file" });
}
} else {
FR.readAsText(file);
// For text and markdown files
fileContents.push({ name: file.name, content: evt.target.result });
}
});

if (fileExtension === 'pdf') {
FR.readAsArrayBuffer(file);
} else {
FR.readAsText(file);
}
}

function submitPrompt(event) {
Expand Down Expand Up @@ -303,34 +390,64 @@ function processAndSendMessage(inputValue) {
// Reset file contents and names after sending
fileContents = [];
currentFileNames = [];
images = [];
audios = [];
imageFileMap.clear();
audioFileMap.clear();

// Clear Alpine.js attachedFiles array
const inputContainer = document.querySelector('[x-data*="attachedFiles"]');
if (inputContainer && window.Alpine) {
const alpineData = Alpine.$data(inputContainer);
if (alpineData && alpineData.attachedFiles) {
alpineData.attachedFiles = [];
}
}

// Clear file inputs
document.getElementById("input_image").value = null;
document.getElementById("input_audio").value = null;
document.getElementById("input_file").value = null;
}

function readInputImage() {
if (!this.files || !this.files.length) return;

Array.from(this.files).forEach(file => {
const FR = new FileReader();
readInputImageFile(file);
});
}

FR.addEventListener("load", function(evt) {
images.push(evt.target.result);
});
function readInputImageFile(file) {
const FR = new FileReader();

FR.readAsDataURL(file);
FR.addEventListener("load", function(evt) {
const dataURL = evt.target.result;
images.push(dataURL);
imageFileMap.set(file.name, dataURL);
});

FR.readAsDataURL(file);
}

function readInputAudio() {
if (!this.files || !this.files.length) return;

Array.from(this.files).forEach(file => {
const FR = new FileReader();
readInputAudioFile(file);
});
}

FR.addEventListener("load", function(evt) {
audios.push(evt.target.result);
});
function readInputAudioFile(file) {
const FR = new FileReader();

FR.readAsDataURL(file);
FR.addEventListener("load", function(evt) {
const dataURL = evt.target.result;
audios.push(dataURL);
audioFileMap.set(file.name, dataURL);
});

FR.readAsDataURL(file);
}

async function promptGPT(systemPrompt, input) {
Expand Down Expand Up @@ -395,13 +512,8 @@ async function promptGPT(systemPrompt, input) {
}
});

// reset the form and the files
images = [];
audios = [];
document.getElementById("input_image").value = null;
document.getElementById("input_audio").value = null;
document.getElementById("input_file").value = null;
document.getElementById("fileName").innerHTML = "";
// reset the form and the files (already done in processAndSendMessage)
// images, audios, and file inputs are cleared after sending

// Choose endpoint based on MCP mode
const endpoint = mcpMode ? "v1/mcp/chat/completions" : "v1/chat/completions";
Expand Down
7 changes: 7 additions & 0 deletions core/http/views/backends.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ <h1 class="text-4xl md:text-5xl font-bold text-[#E5E7EB] mb-4">
<span class="font-semibold text-emerald-300" x-text="availableBackends"></span>
<span class="text-[#94A3B8] ml-1">backends available</span>
</div>
<a href="/manage" class="flex items-center bg-[#101827] hover:bg-[#1E293B] rounded-lg px-4 py-2 transition-colors border border-[#8B5CF6]/30 hover:border-[#8B5CF6]/50">
<div class="w-2 h-2 bg-cyan-400 rounded-full mr-2"></div>
<span class="font-semibold text-cyan-300" x-text="installedBackends"></span>
<span class="text-[#94A3B8] ml-1">installed</span>
</a>
<a href="https://localai.io/backends/" target="_blank"
class="inline-flex items-center bg-cyan-600 hover:bg-cyan-700 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-info-circle mr-2"></i>
Expand Down Expand Up @@ -488,6 +493,7 @@ <h3 class="text-xl font-semibold text-gray-900 dark:text-white" x-text="selected
currentPage: 1,
totalPages: 1,
availableBackends: 0,
installedBackends: 0,
selectedBackend: null,
jobProgress: {},
notifications: [],
Expand Down Expand Up @@ -526,6 +532,7 @@ <h3 class="text-xl font-semibold text-gray-900 dark:text-white" x-text="selected
this.currentPage = data.currentPage || 1;
this.totalPages = data.totalPages || 1;
this.availableBackends = data.availableBackends || 0;
this.installedBackends = data.installedBackends || 0;
} catch (error) {
console.error('Error fetching backends:', error);
} finally {
Expand Down
Loading
Loading