Files
store/titles.sune

27 lines
12 KiB
JSON

[
{
"id": "dn7bag8",
"name": "Auto Title",
"pinned": false,
"avatar": "",
"url": "",
"updatedAt": 1756912466542,
"settings": {
"model": "openai/gpt-5",
"temperature": 1,
"top_p": 0.96,
"top_k": 0,
"frequency_penalty": 0,
"presence_penalty": 0,
"repetition_penalty": 1,
"min_p": 0,
"top_a": 0,
"max_tokens": 0,
"verbosity": "",
"reasoning_effort": "low",
"system_prompt": "",
"html": "<!-- Sune: Auto-Title Generator (v4 with Retry Logic) -->\n<div id=\"auto-title-sune-container\">\n <!-- Debugging UI. Change ENABLE_DEBUG to false to disable completely. -->\n <div id=\"auto-title-debug-panel\" class=\"hidden fixed bottom-0 left-0 right-0 z-[100] bg-gray-900/90 text-white backdrop-blur-sm shadow-2xl shadow-black/50 transition-all duration-300 max-h-0\">\n <div class=\"flex flex-col h-full max-h-[40vh]\">\n <div class=\"flex items-center justify-between p-2 border-b border-gray-700 bg-gray-800\">\n <h3 class=\"text-sm font-semibold tracking-wider\">Auto-Title Sune Log</h3>\n <div class=\"flex items-center gap-2\">\n <button id=\"auto-title-manual-trigger\" title=\"Generate Title Manually\" class=\"p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md\">\n <i data-lucide=\"wand-2\" class=\"h-4 w-4\"></i>\n </button>\n <button id=\"auto-title-clear-log\" title=\"Clear Log\" class=\"p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md\">\n <i data-lucide=\"trash-2\" class=\"h-4 w-4\"></i>\n </button>\n <button id=\"auto-title-toggle-panel\" title=\"Minimize\" class=\"p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md\">\n <i data-lucide=\"minus\" class=\"h-4 w-4\"></i>\n </button>\n </div>\n </div>\n <pre id=\"auto-title-log-content\" class=\"flex-1 p-3 text-xs font-mono overflow-y-auto text-gray-300 leading-snug\"></pre>\n </div>\n </div>\n <button id=\"auto-title-debug-fab\" class=\"hidden fixed bottom-4 right-4 z-[99] h-10 w-10 bg-indigo-600 text-white rounded-full flex items-center justify-center shadow-lg hover:bg-indigo-700 active:scale-95 transition-all\">\n <i data-lucide=\"file-terminal\" class=\"h-5 w-5\"></i>\n </button>\n</div>\n\n<script>\n(function() {\n \"use strict\";\n\n // --- CONFIGURATION ---\n const ENABLE_DEBUG = false;\n const MAX_RETRIES = 20; // Number of times to check for the message\n const RETRY_DELAY_MS = 50; // Milliseconds to wait between retries\n \n // --- CONSTANTS ---\n const CHAR_LIMIT = 120000;\n const TITLE_SYSTEM_PROMPT = `You are a title generation expert. Your task is to create a short, concise, and descriptive title for a chat thread based on the user's first message. Rules: The title must be 8 words or less. The title should accurately summarize the main topic or question. Do NOT use quotation marks. Respond ONLY with the generated title.`;\n\n // --- SUNE CONTEXT & DOM ---\n const suneId = window.SUNE.id;\n window.doCreateTitle = false // keep this & the comment\n const suneContainer = document.getElementById('auto-title-sune-container');\n if (!suneContainer) return;\n \n const debugPanel = suneContainer.querySelector('#auto-title-debug-panel');\n const logContent = suneContainer.querySelector('#auto-title-log-content');\n const toggleButton = suneContainer.querySelector('#auto-title-toggle-panel');\n const clearButton = suneContainer.querySelector('#auto-title-clear-log');\n const fab = suneContainer.querySelector('#auto-title-debug-fab');\n const manualTriggerButton = suneContainer.querySelector('#auto-title-manual-trigger');\n\n // --- UTILITY FUNCTIONS ---\n const log = (message, data = null) => {\n if (!ENABLE_DEBUG) return;\n console.log(`[AutoTitleSune]`, message, data !== null ? data : '');\n if (!logContent) return;\n const timestamp = new Date().toLocaleTimeString();\n const line = document.createElement('div');\n line.innerHTML = `<span class=\"text-gray-500\">${timestamp}:</span> <span class=\"text-gray-100\">${escapeHtml(String(message))}</span>`;\n if (data) {\n const dataPre = document.createElement('pre');\n dataPre.className = 'mt-1 p-2 bg-black/50 rounded text-sky-300 text-[11px] overflow-x-auto';\n dataPre.textContent = JSON.stringify(data, null, 2);\n line.appendChild(dataPre);\n }\n logContent.appendChild(line);\n logContent.scrollTop = logContent.scrollHeight;\n };\n const escapeHtml = (unsafe) => unsafe ? unsafe.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\").replace(/'/g, \"&#039;\") : '';\n const partsToText = (parts) => Array.isArray(parts) ? parts.filter(p => p.type === 'text').map(p => p.text).join('\\n') : '';\n\n /**\n * The core logic for generating and setting a thread title. We know this works.\n * @param {string} threadId The ID of the thread to process.\n * @param {string} triggerType A label for logging ('Automatic' or 'Manual').\n */\n const generateAndSetTitle = async (threadId, triggerType) => {\n try {\n log(`Starting title generation for thread ${threadId} (Trigger: ${triggerType})`);\n\n const thread = window.SUNE.getThread(threadId);\n if (!thread) throw new Error(`Could not retrieve thread object for id: ${threadId}`);\n\n const firstUserMessage = thread.messages.find(m => m.role === 'user');\n if (!firstUserMessage || !firstUserMessage.content) {\n throw new Error('No user message object found in the retrieved thread.');\n }\n \n let userText = partsToText(firstUserMessage.content).trim();\n if (!userText) {\n userText = firstUserMessage.content.some(p => p.type !== 'text') ? '(Message with an attachment)' : '';\n }\n if (!userText) throw new Error('User message has no text or attachments. Aborting.');\n\n const truncatedText = userText.substring(0, CHAR_LIMIT);\n log(`Extracted user content (${truncatedText.length} chars).`);\n\n const model = window.SUNE.titleModel;\n const apiKey = model.startsWith('oai:') ? window.SUNE.apiKeyOAI : window.SUNE.apiKeyOR;\n\n if (!model) throw new Error('Title generation model (SUNE.titleModel) is not set.');\n if (!apiKey) throw new Error('API key for the selected provider is not set.');\n\n const payload = { model: model.replace(/^(or:|oai:)/, ''), messages: [{ role: 'system', content: TITLE_SYSTEM_PROMPT }, { role: 'user', content: truncatedText }], max_tokens: 20, temperature: 0.5, stream: false };\n log('Sending API request...', payload);\n \n const response = await fetch(\"https://openrouter.ai/api/v1/chat/completions\", { method: \"POST\", headers: { \"Authorization\": `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" }, body: JSON.stringify(payload) });\n if (!response.ok) {\n throw new Error(`API request failed with status ${response.status}: ${await response.text()}`);\n }\n\n const result = await response.json();\n log('Received API response:', result);\n const newTitle = result.choices?.[0]?.message?.content?.trim().replace(/[\"']/g, '');\n if (!newTitle) throw new Error('Could not extract a valid title from the API response.');\n \n log(`Generated title: \"${newTitle}\"`);\n await window.SUNE.setThreadTitle(threadId, newTitle);\n log(`Successfully set title for thread ${threadId}.`);\n\n } catch (error) {\n log(`An error occurred:`, error.message);\n console.error(error);\n }\n };\n \n /**\n * NEW: Robustly finds the first user message using polling/retries.\n * @param {string} threadId The ID of the thread to process.\n */\n const processThreadWithRetry = async (threadId) => {\n for (let i = 0; i < MAX_RETRIES; i++) {\n log(`Attempt ${i + 1}/${MAX_RETRIES} to find user message in thread ${threadId}...`);\n const thread = window.SUNE.getThread(threadId);\n const userMessageExists = thread?.messages?.some(m => m.role === 'user');\n\n if (userMessageExists) {\n log(`Success! User message found on attempt ${i + 1}.`);\n await generateAndSetTitle(threadId, 'Automatic');\n return; // Exit the retry loop\n }\n\n // If not found, wait before the next attempt\n await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));\n }\n log(`Error: Max retries (${MAX_RETRIES}) reached. Could not find user message in thread ${threadId}. Aborting automatic title generation.`);\n };\n\n // --- EVENT HANDLERS ---\n const handleAutomaticTrigger = (event) => {\n if (window.SUNE.id !== suneId) return;\n const { threadId } = event.detail;\n if (!threadId) {\n log('Error: new-thread event received without a threadId.');\n return;\n }\n // Instead of calling the main logic directly, we call the new retry wrapper.\n processThreadWithRetry(threadId);\n };\n \n const handleManualTrigger = () => {\n const currentThreadId = window.state.currentThreadId;\n if (!currentThreadId) {\n log('Manual Trigger Error: No active thread selected.');\n alert('No active thread. Please start a conversation first.');\n return;\n }\n // Manual trigger can still call the function directly as the state is guaranteed to be ready.\n generateAndSetTitle(currentThreadId, 'Manual');\n };\n\n // --- INITIALIZATION ---\n const initialize = () => {\n if (!suneContainer) return;\n if (ENABLE_DEBUG) {\n fab.classList.remove('hidden');\n lucide.createIcons({ nodes: suneContainer.querySelectorAll('[data-lucide]') });\n const showPanel = () => { fab.classList.add('hidden'); debugPanel.classList.remove('hidden'); setTimeout(() => { debugPanel.classList.add('max-h-[40vh]'); debugPanel.classList.remove('max-h-0'); }, 10); };\n const hidePanel = () => { debugPanel.classList.add('max-h-0'); debugPanel.classList.remove('max-h-[40vh]'); fab.classList.remove('hidden'); setTimeout(() => { if(debugPanel.classList.contains('max-h-0')) debugPanel.classList.add('hidden'); }, 300); };\n fab.addEventListener('click', showPanel);\n toggleButton.addEventListener('click', hidePanel);\n clearButton.addEventListener('click', () => { if(logContent) logContent.innerHTML = ''; log('Log cleared.'); });\n manualTriggerButton.addEventListener('click', handleManualTrigger);\n }\n\n document.addEventListener('sune:new-thread', handleAutomaticTrigger);\n log('Auto-Title Sune (v4 with Retry Logic) initialized.');\n };\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initialize);\n } else {\n initialize();\n }\n})();\n</script>\n",
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune'/>"
}
}
]