Complete Ollama Web UI implementation featuring: Frontend (SvelteKit + Svelte 5 + Tailwind CSS + Skeleton UI): - Chat interface with streaming responses and markdown rendering - Message tree with branching support (edit creates branches) - Vision model support with image upload/paste - Code syntax highlighting with Shiki - Built-in tools: get_current_time, calculate, fetch_url - Function model middleware (functiongemma) for tool routing - IndexedDB storage with Dexie.js - Context window tracking with token estimation - Knowledge base with embeddings (RAG support) - Keyboard shortcuts and responsive design - Export conversations as Markdown/JSON Backend (Go + Gin + SQLite): - RESTful API for conversations and messages - SQLite persistence with branching message tree - Sync endpoints for IndexedDB ↔ SQLite synchronization - URL proxy endpoint for CORS-bypassed web fetching - Health check endpoint - Docker support with host network mode Infrastructure: - Docker Compose for development and production - Vite proxy configuration for Ollama and backend APIs - Hot reload development setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
208 lines
5.0 KiB
Go
208 lines
5.0 KiB
Go
package api
|
|
|
|
import (
|
|
"database/sql"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"ollama-webui-backend/internal/models"
|
|
)
|
|
|
|
// ListChatsHandler returns a handler for listing all chats
|
|
func ListChatsHandler(db *sql.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
includeArchived := c.Query("include_archived") == "true"
|
|
|
|
chats, err := models.ListChats(db, includeArchived)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if chats == nil {
|
|
chats = []models.Chat{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"chats": chats})
|
|
}
|
|
}
|
|
|
|
// GetChatHandler returns a handler for getting a single chat
|
|
func GetChatHandler(db *sql.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
chat, err := models.GetChat(db, id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if chat == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "chat not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, chat)
|
|
}
|
|
}
|
|
|
|
// CreateChatRequest represents the request body for creating a chat
|
|
type CreateChatRequest struct {
|
|
Title string `json:"title"`
|
|
Model string `json:"model"`
|
|
}
|
|
|
|
// CreateChatHandler returns a handler for creating a new chat
|
|
func CreateChatHandler(db *sql.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var req CreateChatRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
return
|
|
}
|
|
|
|
chat := &models.Chat{
|
|
Title: req.Title,
|
|
Model: req.Model,
|
|
}
|
|
|
|
if chat.Title == "" {
|
|
chat.Title = "New Chat"
|
|
}
|
|
|
|
if err := models.CreateChat(db, chat); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, chat)
|
|
}
|
|
}
|
|
|
|
// UpdateChatRequest represents the request body for updating a chat
|
|
type UpdateChatRequest struct {
|
|
Title *string `json:"title,omitempty"`
|
|
Model *string `json:"model,omitempty"`
|
|
Pinned *bool `json:"pinned,omitempty"`
|
|
Archived *bool `json:"archived,omitempty"`
|
|
}
|
|
|
|
// UpdateChatHandler returns a handler for updating a chat
|
|
func UpdateChatHandler(db *sql.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
// Get existing chat
|
|
chat, err := models.GetChat(db, id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if chat == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "chat not found"})
|
|
return
|
|
}
|
|
|
|
// Parse update request
|
|
var req UpdateChatRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
return
|
|
}
|
|
|
|
// Apply updates
|
|
if req.Title != nil {
|
|
chat.Title = *req.Title
|
|
}
|
|
if req.Model != nil {
|
|
chat.Model = *req.Model
|
|
}
|
|
if req.Pinned != nil {
|
|
chat.Pinned = *req.Pinned
|
|
}
|
|
if req.Archived != nil {
|
|
chat.Archived = *req.Archived
|
|
}
|
|
|
|
if err := models.UpdateChat(db, chat); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, chat)
|
|
}
|
|
}
|
|
|
|
// DeleteChatHandler returns a handler for deleting a chat
|
|
func DeleteChatHandler(db *sql.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
if err := models.DeleteChat(db, id); err != nil {
|
|
if err.Error() == "chat not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "chat not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "chat deleted"})
|
|
}
|
|
}
|
|
|
|
// CreateMessageRequest represents the request body for creating a message
|
|
type CreateMessageRequest struct {
|
|
ParentID *string `json:"parent_id,omitempty"`
|
|
Role string `json:"role" binding:"required"`
|
|
Content string `json:"content" binding:"required"`
|
|
SiblingIndex int `json:"sibling_index"`
|
|
}
|
|
|
|
// CreateMessageHandler returns a handler for creating a new message
|
|
func CreateMessageHandler(db *sql.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
chatID := c.Param("id")
|
|
|
|
// Verify chat exists
|
|
chat, err := models.GetChat(db, chatID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if chat == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "chat not found"})
|
|
return
|
|
}
|
|
|
|
var req CreateMessageRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
return
|
|
}
|
|
|
|
// Validate role
|
|
if req.Role != "user" && req.Role != "assistant" && req.Role != "system" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "role must be 'user', 'assistant', or 'system'"})
|
|
return
|
|
}
|
|
|
|
msg := &models.Message{
|
|
ChatID: chatID,
|
|
ParentID: req.ParentID,
|
|
Role: req.Role,
|
|
Content: req.Content,
|
|
SiblingIndex: req.SiblingIndex,
|
|
}
|
|
|
|
if err := models.CreateMessage(db, msg); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, msg)
|
|
}
|
|
}
|