Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c89703531c |
@@ -0,0 +1,34 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
class ThemeCustomizerPlugin:
|
||||
def __init__(self):
|
||||
self.name = "theme_customizer"
|
||||
self.description = "Theme customization plugin for OpenWebUI"
|
||||
self.version = "1.0.0"
|
||||
self.author = "SamiAhmed7777"
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Setup any initial plugin state."""
|
||||
pass
|
||||
|
||||
def register(self, app: FastAPI) -> None:
|
||||
"""Register the theme customizer plugin with OpenWebUI."""
|
||||
# Register frontend components
|
||||
if not hasattr(app.state, "plugin_components"):
|
||||
app.state.plugin_components = {}
|
||||
|
||||
# Theme Customization Component
|
||||
app.state.plugin_components["theme_customizer"] = {
|
||||
"name": "ThemeCustomizer",
|
||||
"path": "/plugins/theme_customizer/components/ThemeCustomizer.svelte",
|
||||
"settings": {
|
||||
"section": "general", # This will make it appear under General
|
||||
"title": "Theme Customization",
|
||||
"icon": "palette",
|
||||
"description": "Customize colors, fonts, and appearance",
|
||||
"order": 300 # This will place it after the general settings
|
||||
}
|
||||
}
|
||||
|
||||
# Create plugin instance
|
||||
plugin = ThemeCustomizerPlugin()
|
||||
@@ -0,0 +1,109 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Button from '$lib/components/ui/button/Button.svelte';
|
||||
import Input from '$lib/components/ui/input/Input.svelte';
|
||||
import Label from '$lib/components/ui/label/Label.svelte';
|
||||
import Card from '$lib/components/ui/card/Card.svelte';
|
||||
import Select from '$lib/components/ui/select/Select.svelte';
|
||||
import { toast } from '$lib/components/ui/toast';
|
||||
|
||||
let primaryColor = '#007AFF';
|
||||
let secondaryColor = '#6B7280';
|
||||
let backgroundColor = '#FFFFFF';
|
||||
let textColor = '#000000';
|
||||
let fontSize = '16px';
|
||||
let currentFont = 'Inter';
|
||||
|
||||
let availableFonts = [
|
||||
'Inter',
|
||||
'Roboto',
|
||||
'Open Sans',
|
||||
'Montserrat',
|
||||
'Lato'
|
||||
];
|
||||
|
||||
async function updateTheme() {
|
||||
try {
|
||||
document.documentElement.style.setProperty('--primary-color', primaryColor);
|
||||
document.documentElement.style.setProperty('--secondary-color', secondaryColor);
|
||||
document.documentElement.style.setProperty('--background-color', backgroundColor);
|
||||
document.documentElement.style.setProperty('--text-color', textColor);
|
||||
document.documentElement.style.setProperty('--font-size', fontSize);
|
||||
document.documentElement.style.setProperty('--font-family', currentFont);
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Theme updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to update theme',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Load current theme values from CSS variables
|
||||
primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || primaryColor;
|
||||
secondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--secondary-color').trim() || secondaryColor;
|
||||
backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--background-color').trim() || backgroundColor;
|
||||
textColor = getComputedStyle(document.documentElement).getPropertyValue('--text-color').trim() || textColor;
|
||||
fontSize = getComputedStyle(document.documentElement).getPropertyValue('--font-size').trim() || fontSize;
|
||||
currentFont = getComputedStyle(document.documentElement).getPropertyValue('--font-family').trim() || currentFont;
|
||||
});
|
||||
</script>
|
||||
|
||||
<Card class="p-6">
|
||||
<h2 class="text-2xl font-bold mb-4">Theme Customization</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Primary Color</Label>
|
||||
<Input type="color" bind:value={primaryColor} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Secondary Color</Label>
|
||||
<Input type="color" bind:value={secondaryColor} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Background Color</Label>
|
||||
<Input type="color" bind:value={backgroundColor} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Text Color</Label>
|
||||
<Input type="color" bind:value={textColor} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Font Size</Label>
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={fontSize}
|
||||
placeholder="e.g., 16px, 1.2rem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Font Family</Label>
|
||||
<Select
|
||||
bind:value={currentFont}
|
||||
options={availableFonts}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
class="w-full mt-4"
|
||||
on:click={updateTheme}
|
||||
>
|
||||
Apply Theme
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let containerStatus = '';
|
||||
let error = '';
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/container/status');
|
||||
const data = await response.json();
|
||||
containerStatus = data.status;
|
||||
} catch (err) {
|
||||
error = 'Failed to fetch container status';
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="container-status">
|
||||
{#if error}
|
||||
<p class="error">{error}</p>
|
||||
{:else}
|
||||
<p>Container Status: {containerStatus}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container-status {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
@@ -1,77 +0,0 @@
|
||||
# OpenWebUI Container Management Plugin
|
||||
|
||||
This plugin adds container management capabilities to OpenWebUI, allowing you to monitor, update, and restart the container directly from the web interface.
|
||||
|
||||
## Features
|
||||
|
||||
- View container status and version
|
||||
- Update container to the latest version
|
||||
- Restart container when needed
|
||||
- User-friendly interface integrated into OpenWebUI settings
|
||||
|
||||
## Installation
|
||||
|
||||
### Installing in Docker-based OpenWebUI
|
||||
|
||||
1. If your OpenWebUI is running in Docker, mount the plugin directory into the container by adding this volume to your docker-compose.yml:
|
||||
```yaml
|
||||
services:
|
||||
openwebui:
|
||||
volumes:
|
||||
- ./custom:/app/custom # Mount the plugin directory
|
||||
```
|
||||
|
||||
2. Copy the `custom` directory to your OpenWebUI installation directory (where your docker-compose.yml is located)
|
||||
|
||||
3. Restart your OpenWebUI container:
|
||||
```bash
|
||||
docker-compose restart openwebui
|
||||
```
|
||||
|
||||
The plugin will be automatically detected and loaded by OpenWebUI when it starts.
|
||||
|
||||
### Dependencies
|
||||
|
||||
The plugin requires the following Python packages which are already included in the OpenWebUI Docker image:
|
||||
- fastapi
|
||||
- docker-py
|
||||
|
||||
## Usage
|
||||
|
||||
1. Navigate to the OpenWebUI settings page
|
||||
2. Look for the "Container Management" section
|
||||
3. You can:
|
||||
- View the current container status and version
|
||||
- Click "Update Container" to update to the latest version
|
||||
- Click "Restart Container" to restart the container
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The plugin exposes the following API endpoints:
|
||||
|
||||
- `GET /api/container/status` - Get container status and version
|
||||
- `POST /api/container/update` - Update the container
|
||||
- `POST /api/container/restart` - Restart the container
|
||||
|
||||
## Development
|
||||
|
||||
To modify or extend the plugin:
|
||||
|
||||
1. Frontend components are in `custom/components/`
|
||||
2. Backend API routes are in `custom/api/`
|
||||
3. Container management logic is in `custom/container_manager.py`
|
||||
4. Plugin registration is handled in `custom/plugin.py`
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Make changes to the plugin code
|
||||
2. The changes will be reflected immediately in the OpenWebUI container due to the volume mount
|
||||
3. If you modify Python files, you'll need to restart the OpenWebUI container:
|
||||
```bash
|
||||
docker-compose restart openwebui
|
||||
```
|
||||
4. Frontend changes (Svelte components) will be hot-reloaded automatically
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to submit issues and pull requests to improve the plugin.
|
||||
@@ -1,43 +0,0 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from typing import Dict
|
||||
from ..container_manager import ContainerManager
|
||||
|
||||
router = APIRouter()
|
||||
container_manager = ContainerManager()
|
||||
|
||||
@router.get("/status")
|
||||
async def get_container_status() -> Dict:
|
||||
"""Get the current status and version of the container."""
|
||||
try:
|
||||
status = container_manager.get_status()
|
||||
version = container_manager.get_version()
|
||||
return {
|
||||
"status": status,
|
||||
"version": version
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/update")
|
||||
async def update_container() -> Dict:
|
||||
"""Update the container to the latest version."""
|
||||
try:
|
||||
success = container_manager.update()
|
||||
return {
|
||||
"success": success,
|
||||
"message": "Container updated successfully" if success else "Update failed"
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/restart")
|
||||
async def restart_container() -> Dict:
|
||||
"""Restart the container."""
|
||||
try:
|
||||
success = container_manager.restart()
|
||||
return {
|
||||
"success": success,
|
||||
"message": "Container restarted successfully" if success else "Restart failed"
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -1,121 +0,0 @@
|
||||
{/* ContainerSettings.svelte */}
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { toast } from '$lib/components/ui/toaster';
|
||||
|
||||
let containerStatus = '';
|
||||
let containerVersion = '';
|
||||
let isUpdating = false;
|
||||
let isRestarting = false;
|
||||
|
||||
async function fetchContainerStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/container/status');
|
||||
const data = await response.json();
|
||||
containerStatus = data.status;
|
||||
containerVersion = data.version;
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to fetch container status',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdate() {
|
||||
isUpdating = true;
|
||||
try {
|
||||
const response = await fetch('/api/container/update', {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Container updated successfully'
|
||||
});
|
||||
await fetchContainerStatus();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to update container',
|
||||
variant: 'destructive'
|
||||
});
|
||||
} finally {
|
||||
isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRestart() {
|
||||
isRestarting = true;
|
||||
try {
|
||||
const response = await fetch('/api/container/restart', {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Container restarted successfully'
|
||||
});
|
||||
await fetchContainerStatus();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to restart container',
|
||||
variant: 'destructive'
|
||||
});
|
||||
} finally {
|
||||
isRestarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchContainerStatus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Card class="p-6">
|
||||
<h2 class="text-2xl font-bold mb-4">Container Management</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Status</Label>
|
||||
<Input value={containerStatus} readonly />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Version</Label>
|
||||
<Input value={containerVersion} readonly />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-4">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={isUpdating}
|
||||
on:click={handleUpdate}
|
||||
>
|
||||
{isUpdating ? 'Updating...' : 'Update Container'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={isRestarting}
|
||||
on:click={handleRestart}
|
||||
>
|
||||
{isRestarting ? 'Restarting...' : 'Restart Container'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -1,228 +0,0 @@
|
||||
"""
|
||||
type: action
|
||||
name: Container Manager
|
||||
description: Manage chat.Sami container updates
|
||||
version: 1.0.0
|
||||
author: chat.Sami
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import json
|
||||
import requests
|
||||
from typing import Dict, Any
|
||||
|
||||
class Action:
|
||||
def __init__(self):
|
||||
self.actions = [
|
||||
{
|
||||
"id": "update_container",
|
||||
"name": "Update Container",
|
||||
"description": "Update chat.Sami to the latest version"
|
||||
},
|
||||
{
|
||||
"id": "container_status",
|
||||
"name": "Container Status",
|
||||
"description": "Get current container status and version"
|
||||
},
|
||||
{
|
||||
"id": "check_updates",
|
||||
"name": "Check Updates",
|
||||
"description": "Check for available updates"
|
||||
}
|
||||
]
|
||||
|
||||
def execute(self, action_id: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
if action_id == "update_container":
|
||||
return self._update_container()
|
||||
elif action_id == "container_status":
|
||||
return self._get_container_status()
|
||||
elif action_id == "check_updates":
|
||||
return self._check_updates()
|
||||
else:
|
||||
raise ValueError(f"Unknown action: {action_id}")
|
||||
|
||||
def _get_container_config(self) -> Dict[str, Any]:
|
||||
"""Get current container configuration"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "inspect", "chat-sami"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
container_info = json.loads(result.stdout)[0]
|
||||
|
||||
# Extract current configuration
|
||||
config = {
|
||||
"port": None,
|
||||
"volumes": [],
|
||||
"env": {}
|
||||
}
|
||||
|
||||
# Get port mapping
|
||||
ports = container_info["HostConfig"]["PortBindings"]
|
||||
if "8080/tcp" in ports:
|
||||
port_info = ports["8080/tcp"][0]
|
||||
config["port"] = port_info["HostPort"]
|
||||
|
||||
# Get volume mappings
|
||||
mounts = container_info["HostConfig"]["Mounts"]
|
||||
for mount in mounts:
|
||||
config["volumes"].append({
|
||||
"source": mount["Source"],
|
||||
"target": mount["Target"]
|
||||
})
|
||||
|
||||
# Get environment variables
|
||||
env_list = container_info["Config"]["Env"]
|
||||
for env in env_list:
|
||||
if "=" in env:
|
||||
key, value = env.split("=", 1)
|
||||
config["env"][key] = value
|
||||
|
||||
return config
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def _get_current_version(self) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "inspect", "chat-sami"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
container_info = json.loads(result.stdout)[0]
|
||||
image_tag = container_info["Config"]["Image"].split(":")[-1]
|
||||
return image_tag
|
||||
except:
|
||||
return "unknown"
|
||||
|
||||
def _get_latest_version(self) -> str:
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://api.github.com/repos/open-webui/open-webui/releases/latest",
|
||||
timeout=5
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()["tag_name"].lstrip('v')
|
||||
return "unknown"
|
||||
except:
|
||||
return "unknown"
|
||||
|
||||
def _check_updates(self) -> Dict[str, Any]:
|
||||
current = self._get_current_version()
|
||||
latest = self._get_latest_version()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"current_version": current,
|
||||
"latest_version": latest,
|
||||
"update_available": latest != "unknown" and current != latest
|
||||
}
|
||||
}
|
||||
|
||||
def _update_container(self) -> Dict[str, Any]:
|
||||
try:
|
||||
# Get current container configuration
|
||||
config = self._get_container_config()
|
||||
if not config:
|
||||
raise Exception("Failed to get current container configuration")
|
||||
|
||||
# Pull the latest image
|
||||
subprocess.run(
|
||||
["docker", "pull", "ghcr.io/open-webui/open-webui:main"],
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
# Stop the current container
|
||||
subprocess.run(
|
||||
["docker", "stop", "chat-sami"],
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
# Remove the old container but keep the volumes
|
||||
subprocess.run(
|
||||
["docker", "rm", "chat-sami"],
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
# Create new container with same configuration
|
||||
cmd = ["docker", "run", "-d", "--name", "chat-sami"]
|
||||
|
||||
# Add port mapping
|
||||
if config["port"]:
|
||||
cmd.extend(["-p", f"{config['port']}:8080"])
|
||||
|
||||
# Add volume mappings
|
||||
for volume in config["volumes"]:
|
||||
cmd.extend(["-v", f"{volume['source']}:{volume['target']}"])
|
||||
|
||||
# Add environment variables
|
||||
for key, value in config["env"].items():
|
||||
cmd.extend(["-e", f"{key}={value}"])
|
||||
|
||||
# Add image name
|
||||
cmd.append("ghcr.io/open-webui/open-webui:main")
|
||||
|
||||
# Run the new container
|
||||
subprocess.run(cmd, check=True, capture_output=True)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Updated to latest version. The page will refresh in a few moments."
|
||||
}
|
||||
except subprocess.CalledProcessError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Update failed: {e.stderr.decode() if e.stderr else str(e)}"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Error during update: {str(e)}"
|
||||
}
|
||||
|
||||
def _get_container_status(self) -> Dict[str, Any]:
|
||||
try:
|
||||
# Get container info
|
||||
result = subprocess.run(
|
||||
["docker", "inspect", "chat-sami"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
container_info = json.loads(result.stdout)[0]
|
||||
|
||||
# Get update status
|
||||
update_info = self._check_updates()
|
||||
current_version = update_info["data"]["current_version"]
|
||||
latest_version = update_info["data"]["latest_version"]
|
||||
update_available = update_info["data"]["update_available"]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"status": container_info["State"]["Status"],
|
||||
"health": container_info["State"]["Health"]["Status"] if "Health" in container_info["State"] else "N/A",
|
||||
"current_version": current_version,
|
||||
"latest_version": latest_version,
|
||||
"update_available": update_available,
|
||||
"created": container_info["Created"]
|
||||
}
|
||||
}
|
||||
except subprocess.CalledProcessError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Failed to get container status: {e.stderr.decode() if e.stderr else str(e)}"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Error: {str(e)}"
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
"""
|
||||
type: settings
|
||||
name: Container Settings
|
||||
description: Manage chat.Sami container settings
|
||||
version: 1.0.0
|
||||
author: chat.Sami
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
import json
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class ContainerSettings(BaseModel):
|
||||
"""Container management settings."""
|
||||
enabled: bool = True
|
||||
|
||||
@router.get("/settings/container")
|
||||
async def get_container_settings() -> Dict[str, Any]:
|
||||
"""Get container management settings."""
|
||||
return {
|
||||
"id": "container",
|
||||
"title": "Container Management",
|
||||
"description": "Manage the chat.Sami container",
|
||||
"component": "ContainerStatus",
|
||||
"settings": ContainerSettings().dict()
|
||||
}
|
||||
|
||||
class Settings:
|
||||
def __init__(self):
|
||||
self.settings = {
|
||||
"container": {
|
||||
"title": "Container Management",
|
||||
"description": "Manage your chat.Sami container",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "custom",
|
||||
"component": "ContainerStatus",
|
||||
"action": "container_status",
|
||||
"refresh_interval": 30, # Refresh every 30 seconds
|
||||
"title": "Container Status"
|
||||
},
|
||||
"update": {
|
||||
"type": "button",
|
||||
"title": "Update Container",
|
||||
"description": "Update chat.Sami to the latest version",
|
||||
"action": "update_container",
|
||||
"buttonText": "Update Now",
|
||||
"confirmText": "Are you sure you want to update the container? The service will restart briefly."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def get_settings(self) -> Dict[str, Any]:
|
||||
return self.settings
|
||||
|
||||
def update_settings(self, settings: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# This is a read-only settings panel
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Settings updated"
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
export async function executeAction<T>(actionId: string, params?: Record<string, unknown>): Promise<T> {
|
||||
// This is a mock implementation for testing
|
||||
const mockResponses = {
|
||||
container_status: {
|
||||
success: true,
|
||||
data: {
|
||||
status: "running",
|
||||
health: "healthy",
|
||||
current_version: "1.0.0",
|
||||
latest_version: "1.1.0",
|
||||
update_available: true,
|
||||
created: new Date().toISOString()
|
||||
}
|
||||
},
|
||||
update_container: {
|
||||
success: true,
|
||||
message: "Update initiated"
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(mockResponses[actionId as keyof typeof mockResponses] as T);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
from fastapi import FastAPI
|
||||
from .api.container_routes import router as container_router
|
||||
|
||||
def register_plugin(app: FastAPI) -> None:
|
||||
"""Register the container management plugin with OpenWebUI."""
|
||||
# Register API routes
|
||||
app.include_router(
|
||||
container_router,
|
||||
prefix="/api/container",
|
||||
tags=["container"]
|
||||
)
|
||||
|
||||
# Register frontend components
|
||||
app.state.plugin_components = app.state.plugin_components or {}
|
||||
app.state.plugin_components["container_settings"] = {
|
||||
"name": "ContainerSettings",
|
||||
"path": "custom/components/ContainerSettings.svelte",
|
||||
"settings": {
|
||||
"title": "Container Management",
|
||||
"icon": "container",
|
||||
"description": "Manage container updates and status"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export interface ContainerStatus {
|
||||
status: string;
|
||||
health: string;
|
||||
current_version: string;
|
||||
latest_version: string;
|
||||
update_available: boolean;
|
||||
created: string;
|
||||
}
|
||||
|
||||
export interface ContainerResponse {
|
||||
success: boolean;
|
||||
data?: ContainerStatus;
|
||||
message?: string;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Button from '$lib/components/ui/button/Button.svelte';
|
||||
import Input from '$lib/components/ui/input/Input.svelte';
|
||||
import Label from '$lib/components/ui/label/Label.svelte';
|
||||
import Card from '$lib/components/ui/card/Card.svelte';
|
||||
import Select from '$lib/components/ui/select/Select.svelte';
|
||||
import { toast } from '$lib/components/ui/toast';
|
||||
|
||||
let primaryColor = '#007AFF';
|
||||
let secondaryColor = '#6B7280';
|
||||
let backgroundColor = '#FFFFFF';
|
||||
let textColor = '#000000';
|
||||
let fontSize = '16px';
|
||||
let currentFont = 'Inter';
|
||||
|
||||
let availableFonts = [
|
||||
'Inter',
|
||||
'Roboto',
|
||||
'Open Sans',
|
||||
'Montserrat',
|
||||
'Lato'
|
||||
];
|
||||
|
||||
async function updateTheme() {
|
||||
try {
|
||||
document.documentElement.style.setProperty('--primary-color', primaryColor);
|
||||
document.documentElement.style.setProperty('--secondary-color', secondaryColor);
|
||||
document.documentElement.style.setProperty('--background-color', backgroundColor);
|
||||
document.documentElement.style.setProperty('--text-color', textColor);
|
||||
document.documentElement.style.setProperty('--font-size', fontSize);
|
||||
document.documentElement.style.setProperty('--font-family', currentFont);
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Theme updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to update theme',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Load current theme values from CSS variables
|
||||
primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || primaryColor;
|
||||
secondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--secondary-color').trim() || secondaryColor;
|
||||
backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--background-color').trim() || backgroundColor;
|
||||
textColor = getComputedStyle(document.documentElement).getPropertyValue('--text-color').trim() || textColor;
|
||||
fontSize = getComputedStyle(document.documentElement).getPropertyValue('--font-size').trim() || fontSize;
|
||||
currentFont = getComputedStyle(document.documentElement).getPropertyValue('--font-family').trim() || currentFont;
|
||||
});
|
||||
</script>
|
||||
|
||||
<Card class="p-6">
|
||||
<h2 class="text-2xl font-bold mb-4">Theme Customization</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Primary Color</Label>
|
||||
<Input type="color" bind:value={primaryColor} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Secondary Color</Label>
|
||||
<Input type="color" bind:value={secondaryColor} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Background Color</Label>
|
||||
<Input type="color" bind:value={backgroundColor} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Text Color</Label>
|
||||
<Input type="color" bind:value={textColor} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Font Size</Label>
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={fontSize}
|
||||
placeholder="e.g., 16px, 1.2rem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>Font Family</Label>
|
||||
<Select
|
||||
bind:value={currentFont}
|
||||
options={availableFonts}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
class="w-full mt-4"
|
||||
on:click={updateTheme}
|
||||
>
|
||||
Apply Theme
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
Reference in New Issue
Block a user