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>
151 lines
4.1 KiB
Go
151 lines
4.1 KiB
Go
package api
|
|
|
|
import (
|
|
"database/sql"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"ollama-webui-backend/internal/models"
|
|
)
|
|
|
|
// PushChangesRequest represents the request body for pushing changes
|
|
type PushChangesRequest struct {
|
|
Chats []models.Chat `json:"chats"`
|
|
Messages []models.Message `json:"messages"`
|
|
}
|
|
|
|
// PushChangesHandler returns a handler for pushing changes from client
|
|
func PushChangesHandler(db *sql.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var req PushChangesRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
return
|
|
}
|
|
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start transaction"})
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Process chats
|
|
for _, chat := range req.Chats {
|
|
// Check if chat exists
|
|
var existingVersion int64
|
|
err := tx.QueryRow("SELECT sync_version FROM chats WHERE id = ?", chat.ID).Scan(&existingVersion)
|
|
|
|
if err == sql.ErrNoRows {
|
|
// Insert new chat
|
|
_, err = tx.Exec(`
|
|
INSERT INTO chats (id, title, model, pinned, archived, created_at, updated_at, sync_version)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
chat.ID, chat.Title, chat.Model, chat.Pinned, chat.Archived,
|
|
chat.CreatedAt, chat.UpdatedAt, chat.SyncVersion,
|
|
)
|
|
} else if err == nil && chat.SyncVersion > existingVersion {
|
|
// Update existing chat if incoming version is higher
|
|
_, err = tx.Exec(`
|
|
UPDATE chats SET title = ?, model = ?, pinned = ?, archived = ?,
|
|
updated_at = ?, sync_version = ?
|
|
WHERE id = ?`,
|
|
chat.Title, chat.Model, chat.Pinned, chat.Archived,
|
|
chat.UpdatedAt, chat.SyncVersion, chat.ID,
|
|
)
|
|
}
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to sync chat: " + err.Error()})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Process messages
|
|
for _, msg := range req.Messages {
|
|
// Check if message exists
|
|
var existingVersion int64
|
|
err := tx.QueryRow("SELECT sync_version FROM messages WHERE id = ?", msg.ID).Scan(&existingVersion)
|
|
|
|
if err == sql.ErrNoRows {
|
|
// Insert new message
|
|
_, err = tx.Exec(`
|
|
INSERT INTO messages (id, chat_id, parent_id, role, content, sibling_index, created_at, sync_version)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
msg.ID, msg.ChatID, msg.ParentID, msg.Role, msg.Content,
|
|
msg.SiblingIndex, msg.CreatedAt, msg.SyncVersion,
|
|
)
|
|
} else if err == nil && msg.SyncVersion > existingVersion {
|
|
// Update existing message if incoming version is higher
|
|
_, err = tx.Exec(`
|
|
UPDATE messages SET content = ?, sibling_index = ?, sync_version = ?
|
|
WHERE id = ?`,
|
|
msg.Content, msg.SiblingIndex, msg.SyncVersion, msg.ID,
|
|
)
|
|
}
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to sync message: " + err.Error()})
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to commit transaction"})
|
|
return
|
|
}
|
|
|
|
// Get current max sync version
|
|
maxVersion, err := models.GetMaxSyncVersion(db)
|
|
if err != nil {
|
|
maxVersion = 0
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "changes pushed successfully",
|
|
"sync_version": maxVersion,
|
|
})
|
|
}
|
|
}
|
|
|
|
// PullChangesHandler returns a handler for pulling changes from server
|
|
func PullChangesHandler(db *sql.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
sinceVersionStr := c.Query("since_version")
|
|
var sinceVersion int64 = 0
|
|
|
|
if sinceVersionStr != "" {
|
|
var err error
|
|
sinceVersion, err = strconv.ParseInt(sinceVersionStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid since_version parameter"})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Get changed chats
|
|
chats, err := models.GetChangedChats(db, sinceVersion)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if chats == nil {
|
|
chats = []models.Chat{}
|
|
}
|
|
|
|
// Get current max sync version
|
|
maxVersion, err := models.GetMaxSyncVersion(db)
|
|
if err != nil {
|
|
maxVersion = 0
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"chats": chats,
|
|
"sync_version": maxVersion,
|
|
})
|
|
}
|
|
}
|