Files
vessel/backend/internal/api/chats.go
vikingowl de835b7af7 feat: initial commit - Ollama WebUI with tools, sync, and backend
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>
2025-12-31 08:11:33 +01:00

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)
}
}