191 lines
4.0 KiB
TypeScript
191 lines
4.0 KiB
TypeScript
import { atom } from 'nanostores'
|
|
|
|
import type { ComposerAttachment } from './composer'
|
|
|
|
export interface QueuedPromptEntry {
|
|
id: string
|
|
text: string
|
|
attachments: ComposerAttachment[]
|
|
queuedAt: number
|
|
}
|
|
|
|
type QueueState = Record<string, QueuedPromptEntry[]>
|
|
|
|
const STORAGE_KEY = 'hermes.desktop.composerQueue.v1'
|
|
|
|
const load = (): QueueState => {
|
|
if (typeof window === 'undefined') {
|
|
return {}
|
|
}
|
|
|
|
try {
|
|
const raw = window.localStorage.getItem(STORAGE_KEY)
|
|
const parsed = raw ? JSON.parse(raw) : null
|
|
|
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as QueueState) : {}
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
const save = (state: QueueState) => {
|
|
if (typeof window === 'undefined') {
|
|
return
|
|
}
|
|
|
|
try {
|
|
if (Object.keys(state).length === 0) {
|
|
window.localStorage.removeItem(STORAGE_KEY)
|
|
} else {
|
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
|
}
|
|
} catch {
|
|
// best-effort: storage may be unavailable, queue still works in-memory
|
|
}
|
|
}
|
|
|
|
export const $queuedPromptsBySession = atom<QueueState>(load())
|
|
|
|
const writeSession = (sid: string, queue: QueuedPromptEntry[]) => {
|
|
const current = $queuedPromptsBySession.get()
|
|
const next = { ...current }
|
|
|
|
if (queue.length === 0) {
|
|
delete next[sid]
|
|
} else {
|
|
next[sid] = queue
|
|
}
|
|
|
|
$queuedPromptsBySession.set(next)
|
|
save(next)
|
|
}
|
|
|
|
const sidOf = (key: string | null | undefined): null | string => {
|
|
const trimmed = key?.trim()
|
|
|
|
return trimmed ? trimmed : null
|
|
}
|
|
|
|
const queueFor = (sid: string) => $queuedPromptsBySession.get()[sid] ?? []
|
|
|
|
const nextId = () => `queued-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
|
|
const cloneAttachments = (attachments: ComposerAttachment[]) => attachments.map(a => ({ ...a }))
|
|
|
|
export const getQueuedPrompts = (key: string | null | undefined): QueuedPromptEntry[] => {
|
|
const sid = sidOf(key)
|
|
|
|
return sid ? queueFor(sid) : []
|
|
}
|
|
|
|
export const enqueueQueuedPrompt = (
|
|
key: string | null | undefined,
|
|
payload: { text: string; attachments: ComposerAttachment[] }
|
|
): null | QueuedPromptEntry => {
|
|
const sid = sidOf(key)
|
|
|
|
if (!sid) {
|
|
return null
|
|
}
|
|
|
|
const entry: QueuedPromptEntry = {
|
|
id: nextId(),
|
|
text: payload.text,
|
|
attachments: cloneAttachments(payload.attachments),
|
|
queuedAt: Date.now()
|
|
}
|
|
|
|
writeSession(sid, [...queueFor(sid), entry])
|
|
|
|
return entry
|
|
}
|
|
|
|
export const dequeueQueuedPrompt = (key: string | null | undefined): null | QueuedPromptEntry => {
|
|
const sid = sidOf(key)
|
|
|
|
if (!sid) {
|
|
return null
|
|
}
|
|
|
|
const [head, ...rest] = queueFor(sid)
|
|
|
|
if (!head) {
|
|
return null
|
|
}
|
|
|
|
writeSession(sid, rest)
|
|
|
|
return head
|
|
}
|
|
|
|
export const removeQueuedPrompt = (key: string | null | undefined, id: string): boolean => {
|
|
const sid = sidOf(key)
|
|
|
|
if (!sid) {
|
|
return false
|
|
}
|
|
|
|
const queue = queueFor(sid)
|
|
const next = queue.filter(e => e.id !== id)
|
|
|
|
if (next.length === queue.length) {
|
|
return false
|
|
}
|
|
|
|
writeSession(sid, next)
|
|
|
|
return true
|
|
}
|
|
|
|
export const updateQueuedPrompt = (
|
|
key: string | null | undefined,
|
|
id: string,
|
|
update: { text: string; attachments?: ComposerAttachment[] }
|
|
): boolean => {
|
|
const sid = sidOf(key)
|
|
|
|
if (!sid) {
|
|
return false
|
|
}
|
|
|
|
const queue = queueFor(sid)
|
|
let changed = false
|
|
|
|
const next = queue.map(entry => {
|
|
if (entry.id !== id) {
|
|
return entry
|
|
}
|
|
|
|
const attachments = update.attachments ? cloneAttachments(update.attachments) : entry.attachments
|
|
|
|
if (entry.text === update.text && !update.attachments) {
|
|
return entry
|
|
}
|
|
|
|
changed = true
|
|
|
|
return { ...entry, text: update.text, attachments }
|
|
})
|
|
|
|
if (!changed) {
|
|
return false
|
|
}
|
|
|
|
writeSession(sid, next)
|
|
|
|
return true
|
|
}
|
|
|
|
export const updateQueuedPromptText = (key: string | null | undefined, id: string, text: string): boolean =>
|
|
updateQueuedPrompt(key, id, { text })
|
|
|
|
export const clearQueuedPrompts = (key: string | null | undefined) => {
|
|
const sid = sidOf(key)
|
|
|
|
if (!sid || !(sid in $queuedPromptsBySession.get())) {
|
|
return
|
|
}
|
|
|
|
writeSession(sid, [])
|
|
}
|