music: 加 LLM chat、笔记 tab 化、歌单/标签
chat(右边栏): - chat_messages 表 per piece,OpenAI 兼容 /v1/chat/completions stream:true - backend SSE forward delta,结束时落库 user + assistant - system prompt 注入曲目 (title/artist/category/notes/lyrics 截 4KB) - 网关同 mochi/config.yaml: gemma-4-31b-it on 3.135.65.204:8848,token 走 k8s Secret chat-creds - reqwest client 去掉全局 timeout(chat 流可能跑很久),chord sidecar 调用改 per-request timeout 笔记: 从右 sidebar 移到独立 tab "笔记" 歌单 + tag: - playlists / playlist_pieces / tags / piece_tags 表,CRUD API - PATCH piece 接 tags 数组(按名字 upsert) - list pieces 加 ?tag/?playlist 过滤 + 返回 tags 列表 - 顶 bar filterbar:歌单 + 标签 chip 切换;"+ 新歌单" prompt 创建 - EditView 加 tag 编辑(chip + 自动补全)+ 加入/移除歌单
This commit is contained in:
@@ -8,8 +8,12 @@ async function jsonOrThrow(res) {
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export function listPieces() {
|
||||
return fetch('/api/pieces').then(jsonOrThrow)
|
||||
export function listPieces({ tag, playlist } = {}) {
|
||||
const qs = new URLSearchParams()
|
||||
if (tag) qs.set('tag', tag)
|
||||
if (playlist) qs.set('playlist', String(playlist))
|
||||
const q = qs.toString()
|
||||
return fetch('/api/pieces' + (q ? '?' + q : '')).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
export function getPiece(id) {
|
||||
@@ -65,3 +69,126 @@ export function chordFetch(pieceId) {
|
||||
export function chordStatus(pieceId) {
|
||||
return fetch(`/api/pieces/${pieceId}/chord/status`).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
// ---- chat ----
|
||||
|
||||
export function listChat(pieceId) {
|
||||
return fetch(`/api/pieces/${pieceId}/chat`).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
export function clearChat(pieceId) {
|
||||
return fetch(`/api/pieces/${pieceId}/chat`, { method: 'DELETE' }).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
/**
|
||||
* stream a chat reply.
|
||||
* @param {number} pieceId
|
||||
* @param {string} message
|
||||
* @param {(delta: string) => void} onDelta
|
||||
* @param {AbortSignal} signal
|
||||
* @returns {Promise<{ok: boolean, error?: string}>}
|
||||
*/
|
||||
export async function streamChat(pieceId, message, onDelta, signal) {
|
||||
const resp = await fetch(`/api/pieces/${pieceId}/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message }),
|
||||
signal,
|
||||
})
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text().catch(() => '')
|
||||
return { ok: false, error: text || `${resp.status}` }
|
||||
}
|
||||
const reader = resp.body.getReader()
|
||||
const dec = new TextDecoder()
|
||||
let buf = ''
|
||||
let lastEvent = 'message'
|
||||
let errorMsg = ''
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buf += dec.decode(value, { stream: true })
|
||||
let idx
|
||||
while ((idx = buf.indexOf('\n')) >= 0) {
|
||||
const line = buf.slice(0, idx)
|
||||
buf = buf.slice(idx + 1)
|
||||
// SSE 协议:空行 = event 边界(已在内容中处理);event:/data: 各自一行
|
||||
if (line.startsWith('event:')) {
|
||||
lastEvent = line.slice(6).trim()
|
||||
} else if (line.startsWith('data:')) {
|
||||
const data = line.slice(5).replace(/^ /, '')
|
||||
if (lastEvent === 'error') {
|
||||
errorMsg = data
|
||||
} else if (lastEvent === 'done') {
|
||||
// 不带 data 也算结束
|
||||
} else {
|
||||
onDelta(data)
|
||||
}
|
||||
} else if (line === '') {
|
||||
lastEvent = 'message'
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorMsg) return { ok: false, error: errorMsg }
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
// ---- tags ----
|
||||
|
||||
export function listTags() {
|
||||
return fetch('/api/tags').then(jsonOrThrow)
|
||||
}
|
||||
|
||||
export function createTag(name) {
|
||||
return fetch('/api/tags', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
}).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
export function deleteTag(id) {
|
||||
return fetch(`/api/tags/${id}`, { method: 'DELETE' }).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
// ---- playlists ----
|
||||
|
||||
export function listPlaylists() {
|
||||
return fetch('/api/playlists').then(jsonOrThrow)
|
||||
}
|
||||
|
||||
export function createPlaylist(name, description) {
|
||||
return fetch('/api/playlists', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description: description || null }),
|
||||
}).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
export function getPlaylist(id) {
|
||||
return fetch(`/api/playlists/${id}`).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
export function patchPlaylist(id, body) {
|
||||
return fetch(`/api/playlists/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
export function deletePlaylist(id) {
|
||||
return fetch(`/api/playlists/${id}`, { method: 'DELETE' }).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
export function playlistAddPiece(playlistId, pieceId) {
|
||||
return fetch(`/api/playlists/${playlistId}/pieces`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ piece_id: pieceId }),
|
||||
}).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
export function playlistRemovePiece(playlistId, pieceId) {
|
||||
return fetch(`/api/playlists/${playlistId}/pieces/${pieceId}`, { method: 'DELETE' }).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user