Skip to content

Commit

Permalink
Add Custom Sorting to Avatar TTS
Browse files Browse the repository at this point in the history
  • Loading branch information
roffidaijoubu committed Dec 19, 2024
1 parent d94d373 commit 950c60d
Show file tree
Hide file tree
Showing 8 changed files with 330 additions and 7 deletions.
68 changes: 62 additions & 6 deletions .api_docs/backend_endpoints/avatar.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ Response: {
"voice_id": "en_au_001",
"provider": "tiktok"
}
]
],
"sort_order": 0
}
],
"current_id": "avatar_1734429111883435200"
]
}
```
Note: Avatars are returned sorted by their `sort_order` field in ascending order. If multiple avatars have the same `sort_order`, they are sorted by `created_at`.

2. **Get Single Avatar**
```http
Expand All @@ -53,7 +54,8 @@ Response: {
"voice_id": "en_female_emotional",
"provider": "tiktok"
}
]
],
"sort_order": 1
}
```

Expand Down Expand Up @@ -99,7 +101,8 @@ Response: {
"voice_id": "en_female_emotional",
"provider": "tiktok"
}
]
],
"sort_order": 2
}
]
}
Expand All @@ -119,6 +122,59 @@ Response: {
"is_default": false,
"is_active": false,
"created_at": 1710691200,
"tts_voices": null
"tts_voices": null,
"sort_order": 3
}
```
Note: New avatars are automatically assigned a `sort_order` value of (highest existing sort_order + 1)

7. **Update Avatar Sort Order**
```http
PUT /api/avatars/{id}/sort
Request: {
"sort_order": 5
}
Response: 200 OK
Error Cases:
- 400: Invalid request body
- 404: Avatar not found
```
Note: When updating sort order, the change is immediately broadcasted to all connected clients via the SSE connection.

## SSE Broadcasts

The server sends Server-Sent Events (SSE) to notify clients of avatar updates. Connect to the `/sse` endpoint to receive these events.

1. **Avatar Update Broadcast**
```http
event: avatar_update
data: {
"type": "avatar_update",
"data": {
"avatars": [
{
"id": "avatar_1734429111883435200",
"name": "Active Avatar",
"description": "Currently active avatar",
"states": {
"idle": "/avatars/active_idle.png",
"talking": "/avatars/active_talking.gif"
},
"is_default": false,
"is_active": true,
"created_at": 1710691200,
"tts_voices": [
{
"voice_id": "en_female_emotional",
"provider": "tiktok"
}
],
"sort_order": 5
}
// ... other active avatars
]
}
}
```
Note: The `avatar_update` event is sent whenever an avatar is created, updated, deleted, or when sort orders change. The data contains only the active avatars in their current sorted order.
```
1 change: 1 addition & 0 deletions assets/control.html
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
<table class="tts-table">
<thead>
<tr>
<th style="width: 30px"></th>
<th>Active</th>
<th>Idle State</th>
<th>Talking State</th>
Expand Down
48 changes: 48 additions & 0 deletions assets/css/control.css
Original file line number Diff line number Diff line change
Expand Up @@ -1467,4 +1467,52 @@ button:not(:disabled):hover {
padding: var(--spacing-xl);
text-align: center;
color: var(--text-secondary);
}

/* Add drag handle styles */
.drag-handle {
cursor: move;
color: var(--text-secondary);
opacity: 0.5;
transition: opacity 0.2s ease;
width: 30px !important;
text-align: center;
}

.drag-handle:hover {
opacity: 1;
}

/* Style for dragging state */
.avatar-row.dragging {
cursor: move;
box-shadow: var(--shadow-md);
z-index: 1000;
}

.avatar-row.ui-sortable-helper {
display: table;
border-collapse: collapse;
width: 100%;
}

/* Ensure table header stays in place */
.tts-table thead th {
background: var(--color-secondary);
position: sticky;
top: 0;
z-index: 2;
}

/* Add placeholder styles */
.ui-sortable-placeholder {
visibility: visible !important;
background: var(--color-secondary) !important;
border: 2px dashed var(--border-color) !important;
height: 82px !important; /* Match row height */
}

/* Prevent text selection while dragging */
.avatar-row.dragging * {
user-select: none;
}
74 changes: 74 additions & 0 deletions assets/js/config-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,80 @@ class ConfigManager {

// Setup toggle button functionality
this.setupToggleButton();

// Setup event listeners for all inputs
this.setupEventListeners();
}

setupEventListeners() {
// Setup range input listeners
Object.entries(this.configInputs).forEach(([key, config]) => {
if (config.input) {
config.input.on('input', (e) => {
const value = e.target.value;
config.value.text(value);
config.apply(value);
this.saveConfig();
});
}
});

// Setup position button listeners
$('.position-btn').on('click', (e) => {
const $btn = $(e.currentTarget);
$('.position-btn').removeClass('bg-white/20');
$btn.addClass('bg-white/20');

const justify = $btn.data('justify');
const align = $btn.data('align');
this.configInputs.containerPosition.value = { justify, align };
this.configInputs.containerPosition.apply({ justify, align });
this.saveConfig();
});

// Setup stacking order toggle
$('#stackingOrderToggle').on('click', (e) => {
const $toggle = $(e.currentTarget);
const currentValue = $toggle.data('reversed');
const newValue = !currentValue;

$toggle.data('reversed', newValue);
$toggle.find('span').text(newValue ? 'Last Avatar on Top' : 'First Avatar on Top');
$toggle.find('svg').toggleClass('rotate-180', newValue);

this.configInputs.stackingOrder.value = newValue;
this.configInputs.stackingOrder.apply(newValue);
this.saveConfig();
});

// Setup increase/decrease all buttons
$('#increaseAll').on('click', () => {
Object.entries(this.configInputs).forEach(([key, config]) => {
if (config.input) {
const input = config.input[0];
const step = parseFloat(input.step) || 1;
const newValue = Math.min(parseFloat(input.value) + step, input.max);
input.value = newValue;
config.value.text(newValue);
config.apply(newValue);
}
});
this.saveConfig();
});

$('#decreaseAll').on('click', () => {
Object.entries(this.configInputs).forEach(([key, config]) => {
if (config.input) {
const input = config.input[0];
const step = parseFloat(input.step) || 1;
const newValue = Math.max(parseFloat(input.value) - step, input.min);
input.value = newValue;
config.value.text(newValue);
config.apply(newValue);
}
});
this.saveConfig();
});
}

setupToggleButton() {
Expand Down
73 changes: 73 additions & 0 deletions assets/js/modules/AvatarManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class AvatarManager {
this.createUploadModal();
this.setupAddAvatarButton();
this.setupVoiceModal();
this.setupSortable();
}

createUploadModal() {
Expand Down Expand Up @@ -128,6 +129,11 @@ export class AvatarManager {

return `
<tr class="avatar-row" data-avatar-id="${avatar.id}">
<td class="drag-handle">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 6h8M4 10h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</td>
<td>
<input type="checkbox"
class="avatar-active-toggle"
Expand Down Expand Up @@ -524,4 +530,71 @@ export class AvatarManager {
alert('Failed to save voice settings');
}
}

setupSortable() {
$('#avatar-list').sortable({
handle: '.drag-handle',
axis: 'y',
containment: 'parent',
helper: function(e, tr) {
// Create a helper that maintains cell widths
const $originals = tr.children();
const $helper = tr.clone();
$helper.children().each(function(index) {
$(this).width($originals.eq(index).width());
});
return $helper;
},
start: function(e, ui) {
// Add a class to show we're dragging
ui.item.addClass('dragging');
// Store the original background color
ui.item.data('oldBackground', ui.item.css('background-color'));
// Add a subtle background color while dragging
ui.item.css('background-color', 'var(--color-secondary)');
},
stop: async (e, ui) => {
// Remove dragging class and restore background
ui.item.removeClass('dragging');
ui.item.css('background-color', ui.item.data('oldBackground'));

// Get all avatar rows and their new positions
const rows = $('#avatar-list tr').toArray();
const updates = rows.map((row, index) => ({
id: $(row).data('avatarId'),
sort_order: index
}));

// Update sort order for each changed avatar
for (const update of updates) {
try {
const response = await fetch(`/api/avatars/${update.id}/sort`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sort_order: update.sort_order
})
});

if (!response.ok) {
throw new Error('Failed to update sort order');
}
} catch (error) {
console.error('Error updating sort order:', error);
// Reload avatars to restore original order
this.loadAvatars();
return;
}
}

// Update local state to match new order
this.avatars = rows.map(row =>
this.avatars.find(a => a.id === $(row).data('avatarId'))
);
}
});
$('#avatar-list').disableSelection();
}
}
16 changes: 15 additions & 1 deletion avatar/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"log"
"sort"
"time"

"go.etcd.io/bbolt"
Expand Down Expand Up @@ -91,7 +92,20 @@ func (s *Storage) ListAvatars() ([]Avatar, error) {
})
})

return avatars, err
if err != nil {
return nil, err
}

// Sort avatars by sort_order
sort.Slice(avatars, func(i, j int) bool {
// If sort_order is the same, sort by creation time
if avatars[i].SortOrder == avatars[j].SortOrder {
return avatars[i].CreatedAt < avatars[j].CreatedAt
}
return avatars[i].SortOrder < avatars[j].SortOrder
})

return avatars, nil
}

// SaveConfig saves avatar configuration
Expand Down
1 change: 1 addition & 0 deletions avatar/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Avatar struct {
IsActive bool `json:"is_active"`
CreatedAt int64 `json:"created_at"`
TTSVoices []types.TTSVoice `json:"tts_voices"`
SortOrder int `json:"sort_order"`
}

// AvatarList represents a list of avatars with metadata
Expand Down
Loading

0 comments on commit 950c60d

Please sign in to comment.