@ssemble/mcp-server 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -1
- package/server.json +3 -3
- package/src/apps/dist/game-videos-app.html +163 -0
- package/src/apps/dist/meme-hooks-app.html +165 -0
- package/src/apps/dist/music-app.html +174 -0
- package/src/apps/dist/shorts-app.html +218 -0
- package/src/apps/dist/templates-app.html +157 -0
- package/src/apps/game-videos-app.html +92 -0
- package/src/apps/game-videos-app.js +102 -0
- package/src/apps/meme-hooks-app.html +95 -0
- package/src/apps/meme-hooks-app.js +94 -0
- package/src/apps/music-app.html +96 -0
- package/src/apps/music-app.js +151 -0
- package/src/apps/shorts-app.html +135 -0
- package/src/apps/shorts-app.js +102 -0
- package/src/apps/templates-app.html +87 -0
- package/src/apps/templates-app.js +74 -0
- package/src/assets/thumbnails/ali.jpg +0 -0
- package/src/assets/thumbnails/beast.jpg +0 -0
- package/src/assets/thumbnails/box.jpg +0 -0
- package/src/assets/thumbnails/hormozi1.jpg +0 -0
- package/src/assets/thumbnails/hormozi2.jpg +0 -0
- package/src/assets/thumbnails/iman.jpg +0 -0
- package/src/assets/thumbnails/iman2.jpg +0 -0
- package/src/assets/thumbnails/karaoke.jpg +0 -0
- package/src/assets/thumbnails/simple.jpg +0 -0
- package/src/index.js +2 -0
- package/src/resources/app-resources.js +149 -0
- package/src/tools/get-shorts.js +36 -4
- package/src/tools/list-game-videos.js +34 -4
- package/src/tools/list-meme-hooks.js +34 -4
- package/src/tools/list-music.js +34 -4
- package/src/tools/list-templates.js +64 -5
- package/src/utils/format.js +8 -4
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Ssemble Meme Hooks</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
11
|
+
background: var(--color-background-primary, #1a1a2e);
|
|
12
|
+
color: var(--color-text-primary, #e0e0e0);
|
|
13
|
+
padding: 16px;
|
|
14
|
+
}
|
|
15
|
+
h2 { font-size: 18px; margin-bottom: 16px; color: var(--color-text-primary, #fff); }
|
|
16
|
+
.grid {
|
|
17
|
+
display: grid;
|
|
18
|
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
19
|
+
gap: 12px;
|
|
20
|
+
}
|
|
21
|
+
.card {
|
|
22
|
+
background: var(--color-background-secondary, #16213e);
|
|
23
|
+
border-radius: var(--border-radius-md, 8px);
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
border: 2px solid transparent;
|
|
27
|
+
transition: border-color 0.2s, transform 0.15s;
|
|
28
|
+
}
|
|
29
|
+
.card:hover { border-color: var(--color-border-primary, #4a6cf7); transform: translateY(-2px); }
|
|
30
|
+
.card.selected { border-color: var(--color-border-primary, #4a6cf7); box-shadow: 0 0 12px rgba(74,108,247,0.3); }
|
|
31
|
+
.card img {
|
|
32
|
+
width: 100%;
|
|
33
|
+
aspect-ratio: 16/9;
|
|
34
|
+
object-fit: cover;
|
|
35
|
+
display: block;
|
|
36
|
+
background: var(--color-background-tertiary, #0f3460);
|
|
37
|
+
}
|
|
38
|
+
.card .placeholder {
|
|
39
|
+
width: 100%;
|
|
40
|
+
aspect-ratio: 16/9;
|
|
41
|
+
background: var(--color-background-tertiary, #0f3460);
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
color: var(--color-text-secondary, #555);
|
|
46
|
+
font-size: 12px;
|
|
47
|
+
}
|
|
48
|
+
.card-label {
|
|
49
|
+
padding: 8px;
|
|
50
|
+
font-size: 12px;
|
|
51
|
+
white-space: nowrap;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
text-overflow: ellipsis;
|
|
54
|
+
}
|
|
55
|
+
.duration-badge {
|
|
56
|
+
display: inline-block;
|
|
57
|
+
font-size: 10px;
|
|
58
|
+
background: var(--color-background-tertiary, #0f3460);
|
|
59
|
+
padding: 2px 6px;
|
|
60
|
+
border-radius: 4px;
|
|
61
|
+
margin-left: 4px;
|
|
62
|
+
}
|
|
63
|
+
.loading { text-align: center; padding: 40px; color: var(--color-text-secondary, #888); }
|
|
64
|
+
.selected-info {
|
|
65
|
+
margin-top: 16px;
|
|
66
|
+
padding: 12px;
|
|
67
|
+
background: var(--color-background-secondary, #16213e);
|
|
68
|
+
border-radius: var(--border-radius-md, 8px);
|
|
69
|
+
font-size: 13px;
|
|
70
|
+
}
|
|
71
|
+
.pagination {
|
|
72
|
+
display: flex;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
gap: 8px;
|
|
75
|
+
margin-top: 16px;
|
|
76
|
+
}
|
|
77
|
+
.pagination button {
|
|
78
|
+
padding: 6px 16px;
|
|
79
|
+
border-radius: var(--border-radius-md, 6px);
|
|
80
|
+
border: 1px solid var(--color-border-primary, #4a6cf7);
|
|
81
|
+
background: transparent;
|
|
82
|
+
color: var(--color-border-primary, #4a6cf7);
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
font-size: 13px;
|
|
85
|
+
}
|
|
86
|
+
.pagination button:disabled { opacity: 0.4; cursor: default; }
|
|
87
|
+
.pagination button:hover:not(:disabled) { background: var(--color-border-primary, #4a6cf7); color: #fff; }
|
|
88
|
+
.page-info { font-size: 13px; color: var(--color-text-secondary, #888); display: flex; align-items: center; }
|
|
89
|
+
</style>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<div id="app"><div class="loading">Loading meme hooks...</div></div>
|
|
93
|
+
<script type="module" src="./meme-hooks-app.js"></script>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { App } from '@modelcontextprotocol/ext-apps';
|
|
2
|
+
|
|
3
|
+
const appEl = document.getElementById('app');
|
|
4
|
+
const app = new App({ name: 'Ssemble Meme Hooks', version: '1.0.0' });
|
|
5
|
+
|
|
6
|
+
let selectedName = null;
|
|
7
|
+
let cachedData = null;
|
|
8
|
+
|
|
9
|
+
function render(data) {
|
|
10
|
+
if (!data || !data.memeHooks) {
|
|
11
|
+
appEl.innerHTML = '<div class="loading">No meme hook data received.</div>';
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
cachedData = data;
|
|
16
|
+
const { memeHooks, pagination } = data;
|
|
17
|
+
|
|
18
|
+
let html = `<h2>Meme Hooks (${pagination.totalCount} clips)</h2>`;
|
|
19
|
+
html += '<div class="grid">';
|
|
20
|
+
|
|
21
|
+
memeHooks.forEach((h) => {
|
|
22
|
+
const isSelected = selectedName === h.name;
|
|
23
|
+
const classes = ['card'];
|
|
24
|
+
if (isSelected) classes.push('selected');
|
|
25
|
+
|
|
26
|
+
const imgSrc = h.preview || '';
|
|
27
|
+
const imgTag = imgSrc
|
|
28
|
+
? `<img src="${imgSrc}" alt="${h.name}" loading="lazy" />`
|
|
29
|
+
: `<div class="placeholder">No preview</div>`;
|
|
30
|
+
|
|
31
|
+
const dur = h.video_duration ? `${Math.round(h.video_duration)}s` : '';
|
|
32
|
+
|
|
33
|
+
html += `<div class="${classes.join(' ')}" data-name="${h.name}">
|
|
34
|
+
${imgTag}
|
|
35
|
+
<div class="card-label">${h.name}${dur ? `<span class="duration-badge">${dur}</span>` : ''}</div>
|
|
36
|
+
</div>`;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
html += '</div>';
|
|
40
|
+
|
|
41
|
+
if (selectedName) {
|
|
42
|
+
const sel = memeHooks.find((h) => h.name === selectedName);
|
|
43
|
+
if (sel) {
|
|
44
|
+
html += `<div class="selected-info">Selected: <strong>${sel.name}</strong></div>`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (pagination.totalPages > 1) {
|
|
49
|
+
html += `<div class="pagination">
|
|
50
|
+
<button id="prev-btn" ${pagination.page <= 1 ? 'disabled' : ''}>Previous</button>
|
|
51
|
+
<span class="page-info">Page ${pagination.page} / ${pagination.totalPages}</span>
|
|
52
|
+
<button id="next-btn" ${pagination.page >= pagination.totalPages ? 'disabled' : ''}>Next</button>
|
|
53
|
+
</div>`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
appEl.innerHTML = html;
|
|
57
|
+
|
|
58
|
+
appEl.querySelectorAll('.card').forEach((card) => {
|
|
59
|
+
card.addEventListener('click', () => {
|
|
60
|
+
selectedName = card.dataset.name;
|
|
61
|
+
render(cachedData);
|
|
62
|
+
app.updateModelContext({
|
|
63
|
+
text: `User selected meme hook: "${card.dataset.name}". Use this exact memeHookName when creating shorts with memeHook: true.`,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const prevBtn = document.getElementById('prev-btn');
|
|
69
|
+
const nextBtn = document.getElementById('next-btn');
|
|
70
|
+
if (prevBtn) prevBtn.addEventListener('click', () => loadPage(pagination.page - 1));
|
|
71
|
+
if (nextBtn) nextBtn.addEventListener('click', () => loadPage(pagination.page + 1));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function loadPage(page) {
|
|
75
|
+
appEl.innerHTML = '<div class="loading">Loading...</div>';
|
|
76
|
+
try {
|
|
77
|
+
const result = await app.callServerTool({ name: 'list_meme_hooks', arguments: { page, limit: 20 } });
|
|
78
|
+
const data = result.structuredContent || null;
|
|
79
|
+
if (data) render(data);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
appEl.innerHTML = `<div class="loading">Error: ${err.message}</div>`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
app.ontoolresult = (result) => {
|
|
86
|
+
const data = result.structuredContent || null;
|
|
87
|
+
if (data) {
|
|
88
|
+
render(data);
|
|
89
|
+
} else {
|
|
90
|
+
appEl.innerHTML = '<div class="loading">No structured data available.</div>';
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
app.connect();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Ssemble Music</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
11
|
+
background: var(--color-background-primary, #1a1a2e);
|
|
12
|
+
color: var(--color-text-primary, #e0e0e0);
|
|
13
|
+
padding: 16px;
|
|
14
|
+
}
|
|
15
|
+
h2 { font-size: 18px; margin-bottom: 16px; color: var(--color-text-primary, #fff); }
|
|
16
|
+
.track {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
padding: 10px 12px;
|
|
20
|
+
background: var(--color-background-secondary, #16213e);
|
|
21
|
+
border-radius: var(--border-radius-md, 8px);
|
|
22
|
+
margin-bottom: 8px;
|
|
23
|
+
transition: background 0.15s;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
border: 2px solid transparent;
|
|
26
|
+
}
|
|
27
|
+
.track:hover { background: var(--color-background-tertiary, #1a2a50); }
|
|
28
|
+
.track.playing { background: var(--color-background-tertiary, #1a2a50); border-left: 3px solid var(--color-border-primary, #4a6cf7); }
|
|
29
|
+
.track.selected { border-color: var(--color-border-primary, #4a6cf7); }
|
|
30
|
+
.play-btn {
|
|
31
|
+
width: 36px;
|
|
32
|
+
height: 36px;
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
background: var(--color-border-primary, #4a6cf7);
|
|
35
|
+
border: none;
|
|
36
|
+
color: #fff;
|
|
37
|
+
font-size: 14px;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
flex-shrink: 0;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
transition: background 0.15s;
|
|
44
|
+
}
|
|
45
|
+
.play-btn:hover { opacity: 0.85; }
|
|
46
|
+
.play-btn:disabled { opacity: 0.4; cursor: default; }
|
|
47
|
+
.track-info { flex: 1; margin-left: 12px; overflow: hidden; }
|
|
48
|
+
.track-name {
|
|
49
|
+
font-size: 14px;
|
|
50
|
+
font-weight: 500;
|
|
51
|
+
white-space: nowrap;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
text-overflow: ellipsis;
|
|
54
|
+
}
|
|
55
|
+
.track-duration { font-size: 12px; color: var(--color-text-secondary, #888); margin-top: 2px; }
|
|
56
|
+
.progress-bar {
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: 4px;
|
|
59
|
+
background: var(--color-background-tertiary, #0f3460);
|
|
60
|
+
border-radius: 2px;
|
|
61
|
+
margin-top: 6px;
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
}
|
|
64
|
+
.progress-fill {
|
|
65
|
+
height: 100%;
|
|
66
|
+
background: var(--color-border-primary, #4a6cf7);
|
|
67
|
+
border-radius: 2px;
|
|
68
|
+
width: 0%;
|
|
69
|
+
transition: width 0.3s linear;
|
|
70
|
+
}
|
|
71
|
+
.loading { text-align: center; padding: 40px; color: var(--color-text-secondary, #888); }
|
|
72
|
+
.pagination {
|
|
73
|
+
display: flex;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
gap: 8px;
|
|
76
|
+
margin-top: 16px;
|
|
77
|
+
}
|
|
78
|
+
.pagination button {
|
|
79
|
+
padding: 6px 16px;
|
|
80
|
+
border-radius: var(--border-radius-md, 6px);
|
|
81
|
+
border: 1px solid var(--color-border-primary, #4a6cf7);
|
|
82
|
+
background: transparent;
|
|
83
|
+
color: var(--color-border-primary, #4a6cf7);
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
font-size: 13px;
|
|
86
|
+
}
|
|
87
|
+
.pagination button:disabled { opacity: 0.4; cursor: default; }
|
|
88
|
+
.pagination button:hover:not(:disabled) { background: var(--color-border-primary, #4a6cf7); color: #fff; }
|
|
89
|
+
.page-info { font-size: 13px; color: var(--color-text-secondary, #888); display: flex; align-items: center; }
|
|
90
|
+
</style>
|
|
91
|
+
</head>
|
|
92
|
+
<body>
|
|
93
|
+
<div id="app"><div class="loading">Loading music tracks...</div></div>
|
|
94
|
+
<script type="module" src="./music-app.js"></script>
|
|
95
|
+
</body>
|
|
96
|
+
</html>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { App } from '@modelcontextprotocol/ext-apps';
|
|
2
|
+
|
|
3
|
+
const appEl = document.getElementById('app');
|
|
4
|
+
const app = new App({ name: 'Ssemble Music', version: '1.0.0' });
|
|
5
|
+
|
|
6
|
+
let currentAudio = null;
|
|
7
|
+
let currentTrackName = null;
|
|
8
|
+
let selectedTrackName = null;
|
|
9
|
+
let animFrameId = null;
|
|
10
|
+
let cachedData = null;
|
|
11
|
+
|
|
12
|
+
function formatDuration(seconds) {
|
|
13
|
+
const m = Math.floor(seconds / 60);
|
|
14
|
+
const s = Math.round(seconds % 60);
|
|
15
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function safeId(name) {
|
|
19
|
+
return name.replace(/[^a-zA-Z0-9]/g, '-');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function render(data) {
|
|
23
|
+
if (!data || !data.music) {
|
|
24
|
+
appEl.innerHTML = '<div class="loading">No music data received.</div>';
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
cachedData = data;
|
|
29
|
+
const { music, pagination } = data;
|
|
30
|
+
|
|
31
|
+
let html = `<h2>Background Music (${pagination.totalCount} tracks)</h2>`;
|
|
32
|
+
|
|
33
|
+
music.forEach((track) => {
|
|
34
|
+
const isPlaying = currentTrackName === track.name && currentAudio && !currentAudio.paused;
|
|
35
|
+
const isSelected = selectedTrackName === track.name;
|
|
36
|
+
const classes = ['track'];
|
|
37
|
+
if (isPlaying) classes.push('playing');
|
|
38
|
+
if (isSelected) classes.push('selected');
|
|
39
|
+
const hasUrl = !!track.url;
|
|
40
|
+
|
|
41
|
+
html += `<div class="${classes.join(' ')}" data-name="${track.name}" data-url="${track.url || ''}">
|
|
42
|
+
<button class="play-btn" data-name="${track.name}" data-url="${track.url || ''}" ${!hasUrl ? 'disabled' : ''}>
|
|
43
|
+
${isPlaying ? '▮▮' : '▶'}
|
|
44
|
+
</button>
|
|
45
|
+
<div class="track-info">
|
|
46
|
+
<div class="track-name">${track.name}</div>
|
|
47
|
+
<div class="track-duration">${formatDuration(track.duration)}</div>
|
|
48
|
+
<div class="progress-bar">
|
|
49
|
+
<div class="progress-fill" id="progress-${safeId(track.name)}"></div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>`;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (pagination.totalPages > 1) {
|
|
56
|
+
html += `<div class="pagination">
|
|
57
|
+
<button id="prev-btn" ${pagination.page <= 1 ? 'disabled' : ''}>Previous</button>
|
|
58
|
+
<span class="page-info">Page ${pagination.page} / ${pagination.totalPages}</span>
|
|
59
|
+
<button id="next-btn" ${pagination.page >= pagination.totalPages ? 'disabled' : ''}>Next</button>
|
|
60
|
+
</div>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
appEl.innerHTML = html;
|
|
64
|
+
|
|
65
|
+
appEl.querySelectorAll('.play-btn').forEach((btn) => {
|
|
66
|
+
btn.addEventListener('click', (e) => {
|
|
67
|
+
e.stopPropagation();
|
|
68
|
+
togglePlay(btn.dataset.name, btn.dataset.url);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
appEl.querySelectorAll('.track').forEach((el) => {
|
|
73
|
+
el.addEventListener('click', () => {
|
|
74
|
+
selectedTrackName = el.dataset.name;
|
|
75
|
+
render(cachedData);
|
|
76
|
+
app.updateModelContext({
|
|
77
|
+
text: `User selected music track: "${el.dataset.name}". Use this exact musicName when creating shorts.`,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const prevBtn = document.getElementById('prev-btn');
|
|
83
|
+
const nextBtn = document.getElementById('next-btn');
|
|
84
|
+
if (prevBtn) prevBtn.addEventListener('click', () => loadPage(pagination.page - 1));
|
|
85
|
+
if (nextBtn) nextBtn.addEventListener('click', () => loadPage(pagination.page + 1));
|
|
86
|
+
|
|
87
|
+
updateProgress();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function togglePlay(trackName, url) {
|
|
91
|
+
if (currentTrackName === trackName && currentAudio) {
|
|
92
|
+
if (currentAudio.paused) {
|
|
93
|
+
currentAudio.play();
|
|
94
|
+
} else {
|
|
95
|
+
currentAudio.pause();
|
|
96
|
+
}
|
|
97
|
+
render(cachedData);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (currentAudio) {
|
|
102
|
+
currentAudio.pause();
|
|
103
|
+
currentAudio = null;
|
|
104
|
+
cancelAnimationFrame(animFrameId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!url) return;
|
|
108
|
+
|
|
109
|
+
currentTrackName = trackName;
|
|
110
|
+
currentAudio = new Audio(url);
|
|
111
|
+
currentAudio.play();
|
|
112
|
+
currentAudio.addEventListener('ended', () => {
|
|
113
|
+
currentTrackName = null;
|
|
114
|
+
currentAudio = null;
|
|
115
|
+
cancelAnimationFrame(animFrameId);
|
|
116
|
+
render(cachedData);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
render(cachedData);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function updateProgress() {
|
|
123
|
+
if (currentAudio && currentTrackName && !currentAudio.paused) {
|
|
124
|
+
const pct = (currentAudio.currentTime / currentAudio.duration) * 100;
|
|
125
|
+
const bar = document.getElementById(`progress-${safeId(currentTrackName)}`);
|
|
126
|
+
if (bar) bar.style.width = `${pct}%`;
|
|
127
|
+
animFrameId = requestAnimationFrame(updateProgress);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function loadPage(page) {
|
|
132
|
+
appEl.innerHTML = '<div class="loading">Loading...</div>';
|
|
133
|
+
try {
|
|
134
|
+
const result = await app.callServerTool({ name: 'list_music', arguments: { page, limit: 20 } });
|
|
135
|
+
const data = result.structuredContent || null;
|
|
136
|
+
if (data) render(data);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
appEl.innerHTML = `<div class="loading">Error: ${err.message}</div>`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
app.ontoolresult = (result) => {
|
|
143
|
+
const data = result.structuredContent || null;
|
|
144
|
+
if (data) {
|
|
145
|
+
render(data);
|
|
146
|
+
} else {
|
|
147
|
+
appEl.innerHTML = '<div class="loading">No structured data available.</div>';
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
app.connect();
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Ssemble Shorts</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
11
|
+
background: var(--color-background-primary, #1a1a2e);
|
|
12
|
+
color: var(--color-text-primary, #e0e0e0);
|
|
13
|
+
padding: 16px;
|
|
14
|
+
}
|
|
15
|
+
h2 { font-size: 18px; margin-bottom: 8px; color: var(--color-text-primary, #fff); }
|
|
16
|
+
.source-info {
|
|
17
|
+
font-size: 12px;
|
|
18
|
+
color: var(--color-text-secondary, #888);
|
|
19
|
+
margin-bottom: 16px;
|
|
20
|
+
line-height: 1.6;
|
|
21
|
+
}
|
|
22
|
+
.clips-grid {
|
|
23
|
+
display: grid;
|
|
24
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
25
|
+
gap: 16px;
|
|
26
|
+
}
|
|
27
|
+
.clip-card {
|
|
28
|
+
background: var(--color-background-secondary, #16213e);
|
|
29
|
+
border-radius: var(--border-radius-md, 8px);
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
border: 2px solid transparent;
|
|
32
|
+
transition: border-color 0.2s;
|
|
33
|
+
}
|
|
34
|
+
.clip-card:hover { border-color: var(--color-border-primary, #4a6cf7); }
|
|
35
|
+
.clip-card video {
|
|
36
|
+
width: 100%;
|
|
37
|
+
aspect-ratio: 9/16;
|
|
38
|
+
object-fit: contain;
|
|
39
|
+
background: #000;
|
|
40
|
+
display: block;
|
|
41
|
+
max-height: 400px;
|
|
42
|
+
}
|
|
43
|
+
.clip-body { padding: 12px; }
|
|
44
|
+
.clip-title {
|
|
45
|
+
font-size: 14px;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
margin-bottom: 6px;
|
|
48
|
+
line-height: 1.3;
|
|
49
|
+
}
|
|
50
|
+
.clip-meta {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 8px;
|
|
54
|
+
margin-bottom: 8px;
|
|
55
|
+
flex-wrap: wrap;
|
|
56
|
+
}
|
|
57
|
+
.badge {
|
|
58
|
+
display: inline-block;
|
|
59
|
+
font-size: 11px;
|
|
60
|
+
padding: 2px 8px;
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
}
|
|
64
|
+
.badge-duration {
|
|
65
|
+
background: var(--color-background-tertiary, #0f3460);
|
|
66
|
+
color: var(--color-text-primary, #e0e0e0);
|
|
67
|
+
}
|
|
68
|
+
.badge-score {
|
|
69
|
+
color: #fff;
|
|
70
|
+
}
|
|
71
|
+
.badge-score.high { background: #22c55e; }
|
|
72
|
+
.badge-score.medium { background: #eab308; }
|
|
73
|
+
.badge-score.low { background: #ef4444; }
|
|
74
|
+
.viral-bar {
|
|
75
|
+
width: 100%;
|
|
76
|
+
height: 4px;
|
|
77
|
+
background: var(--color-background-tertiary, #0f3460);
|
|
78
|
+
border-radius: 2px;
|
|
79
|
+
margin-bottom: 8px;
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
}
|
|
82
|
+
.viral-fill {
|
|
83
|
+
height: 100%;
|
|
84
|
+
border-radius: 2px;
|
|
85
|
+
transition: width 0.5s ease;
|
|
86
|
+
}
|
|
87
|
+
.viral-fill.high { background: #22c55e; }
|
|
88
|
+
.viral-fill.medium { background: #eab308; }
|
|
89
|
+
.viral-fill.low { background: #ef4444; }
|
|
90
|
+
.clip-desc {
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
color: var(--color-text-secondary, #888);
|
|
93
|
+
line-height: 1.5;
|
|
94
|
+
margin-bottom: 10px;
|
|
95
|
+
display: -webkit-box;
|
|
96
|
+
-webkit-line-clamp: 3;
|
|
97
|
+
-webkit-box-orient: vertical;
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
}
|
|
100
|
+
.clip-actions {
|
|
101
|
+
display: flex;
|
|
102
|
+
gap: 8px;
|
|
103
|
+
}
|
|
104
|
+
.btn {
|
|
105
|
+
padding: 6px 14px;
|
|
106
|
+
border-radius: var(--border-radius-md, 6px);
|
|
107
|
+
border: 1px solid var(--color-border-primary, #4a6cf7);
|
|
108
|
+
background: transparent;
|
|
109
|
+
color: var(--color-border-primary, #4a6cf7);
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
font-size: 12px;
|
|
112
|
+
transition: background 0.15s;
|
|
113
|
+
}
|
|
114
|
+
.btn:hover { background: var(--color-border-primary, #4a6cf7); color: #fff; }
|
|
115
|
+
.btn-primary {
|
|
116
|
+
background: var(--color-border-primary, #4a6cf7);
|
|
117
|
+
color: #fff;
|
|
118
|
+
}
|
|
119
|
+
.btn-primary:hover { opacity: 0.85; }
|
|
120
|
+
.clip-error {
|
|
121
|
+
padding: 12px;
|
|
122
|
+
font-size: 13px;
|
|
123
|
+
color: #ef4444;
|
|
124
|
+
text-align: center;
|
|
125
|
+
}
|
|
126
|
+
.loading { text-align: center; padding: 40px; color: var(--color-text-secondary, #888); }
|
|
127
|
+
.no-clips { text-align: center; padding: 40px; color: var(--color-text-secondary, #888); }
|
|
128
|
+
.rank { font-size: 20px; font-weight: 700; color: var(--color-text-secondary, #555); margin-right: 4px; }
|
|
129
|
+
</style>
|
|
130
|
+
</head>
|
|
131
|
+
<body>
|
|
132
|
+
<div id="app"><div class="loading">Loading shorts...</div></div>
|
|
133
|
+
<script type="module" src="./shorts-app.js"></script>
|
|
134
|
+
</body>
|
|
135
|
+
</html>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { App } from '@modelcontextprotocol/ext-apps';
|
|
2
|
+
|
|
3
|
+
const appEl = document.getElementById('app');
|
|
4
|
+
const app = new App({ name: 'Ssemble Shorts', version: '1.0.0' });
|
|
5
|
+
|
|
6
|
+
function scoreClass(score) {
|
|
7
|
+
if (score >= 80) return 'high';
|
|
8
|
+
if (score >= 50) return 'medium';
|
|
9
|
+
return 'low';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function render(data) {
|
|
13
|
+
if (!data || !data.shorts) {
|
|
14
|
+
appEl.innerHTML = '<div class="loading">No shorts data received.</div>';
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { shorts, title, url, start, end, language, template } = data;
|
|
19
|
+
|
|
20
|
+
if (shorts.length === 0) {
|
|
21
|
+
appEl.innerHTML = '<div class="no-clips">No clips were generated for this request.</div>';
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Sort by viral score descending
|
|
26
|
+
const sorted = [...shorts].sort((a, b) => (b.viral_score || 0) - (a.viral_score || 0));
|
|
27
|
+
|
|
28
|
+
let html = `<h2>Generated Shorts${title ? ` — ${title}` : ''}</h2>`;
|
|
29
|
+
html += '<div class="source-info">';
|
|
30
|
+
if (url) html += `Source: ${url}<br>`;
|
|
31
|
+
if (start != null && end != null) html += `Time range: ${start}s – ${end}s | `;
|
|
32
|
+
if (language) html += `Language: ${language} | `;
|
|
33
|
+
if (template) html += `Template: ${template}`;
|
|
34
|
+
html += '</div>';
|
|
35
|
+
|
|
36
|
+
html += '<div class="clips-grid">';
|
|
37
|
+
|
|
38
|
+
sorted.forEach((clip, i) => {
|
|
39
|
+
const score = clip.viral_score != null ? clip.viral_score : 0;
|
|
40
|
+
const cls = scoreClass(score);
|
|
41
|
+
|
|
42
|
+
if (clip.error && !clip.video_url) {
|
|
43
|
+
html += `<div class="clip-card">
|
|
44
|
+
<div class="clip-error">Clip failed to generate</div>
|
|
45
|
+
<div class="clip-body">
|
|
46
|
+
<div class="clip-title">${clip.title || 'Untitled'}</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>`;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
html += `<div class="clip-card">
|
|
53
|
+
<video src="${clip.video_url}" preload="metadata" controls playsinline></video>
|
|
54
|
+
<div class="clip-body">
|
|
55
|
+
<div class="clip-meta">
|
|
56
|
+
<span class="rank">#${i + 1}</span>
|
|
57
|
+
${clip.duration ? `<span class="badge badge-duration">${Math.round(clip.duration)}s</span>` : ''}
|
|
58
|
+
<span class="badge badge-score ${cls}">Viral: ${score}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="viral-bar"><div class="viral-fill ${cls}" style="width:${score}%"></div></div>
|
|
61
|
+
<div class="clip-title">${clip.title || 'Untitled'}</div>
|
|
62
|
+
${clip.description ? `<div class="clip-desc">${clip.description}</div>` : ''}
|
|
63
|
+
<div class="clip-actions">
|
|
64
|
+
<button class="btn btn-primary download-btn" data-url="${clip.video_url}" data-title="${(clip.title || 'short').replace(/"/g, '')}">Download</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>`;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
html += '</div>';
|
|
71
|
+
appEl.innerHTML = html;
|
|
72
|
+
|
|
73
|
+
// Download buttons
|
|
74
|
+
appEl.querySelectorAll('.download-btn').forEach((btn) => {
|
|
75
|
+
btn.addEventListener('click', (e) => {
|
|
76
|
+
e.stopPropagation();
|
|
77
|
+
const videoUrl = btn.dataset.url;
|
|
78
|
+
app.openLink({ url: videoUrl }).catch(() => {
|
|
79
|
+
// Fallback: anchor tag click
|
|
80
|
+
const a = document.createElement('a');
|
|
81
|
+
a.href = videoUrl;
|
|
82
|
+
a.target = '_blank';
|
|
83
|
+
a.rel = 'noopener';
|
|
84
|
+
document.body.appendChild(a);
|
|
85
|
+
a.click();
|
|
86
|
+
a.remove();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
app.ontoolresult = (result) => {
|
|
94
|
+
const data = result.structuredContent || null;
|
|
95
|
+
if (data) {
|
|
96
|
+
render(data);
|
|
97
|
+
} else {
|
|
98
|
+
appEl.innerHTML = '<div class="loading">No structured data available.</div>';
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
app.connect();
|