2026-03-15 14:51:29 +01:00
const tauriApi = window . _ _TAURI _ _ ;
const invoke = tauriApi ? . core ? . invoke ;
const listen = tauriApi ? . event ? . listen ;
const convertFileSrc = tauriApi ? . core ? . convertFileSrc ;
const confirmDialog = tauriApi ? . dialog ? . confirm ;
2026-05-04 10:13:03 +02:00
const DEFAULT _MASTER _PROMPT = ` You are an expert summarizer. Summarize the following video concisely:
Title : { title }
Transcript :
{ transcript }
Summary : ` ;
const DEFAULT _TRANSLATION _PROMPTS = {
de : ` Translate the following summary into German. Only output the translated summary, no explanation or intro. If it's already in the target language, do nothing but repeat it.
Summary :
{ summary }
Translation : ` ,
jp : ` Translate the following summary into Japanese. Only output the translated summary, no explanation or intro. If it's already in the target language, do nothing but repeat it.
Summary :
{ summary }
Translation : `
} ;
2026-03-15 14:51:29 +01:00
if ( ! invoke || ! listen ) {
throw new Error ( 'Tauri runtime API is unavailable.' ) ;
}
function toWebviewFileUrl ( filePath ) {
if ( ! filePath ) {
return filePath ;
}
if ( typeof convertFileSrc === 'function' ) {
return convertFileSrc ( filePath ) ;
}
return filePath ;
}
window . api = {
getModels : ( ) => invoke ( 'get_models' ) ,
getSummaries : ( ) => invoke ( 'get_summaries' ) ,
2026-05-04 10:13:03 +02:00
summarizeVideo : ( url , useWhisper , model , masterPrompt ) => invoke ( 'summarize_video' , {
2026-03-15 14:51:29 +01:00
request : {
url ,
useWhisper ,
2026-05-04 10:13:03 +02:00
model : model || null ,
masterPrompt : masterPrompt || null
2026-03-15 14:51:29 +01:00
}
} ) ,
openExternal : ( url ) => invoke ( 'open_external' , { url } ) ,
openFile : ( filePath ) => invoke ( 'open_file' , { filePath } ) ,
deleteSummary : ( id ) => invoke ( 'delete_summary' , {
request : { id }
} ) ,
2026-05-04 10:33:38 +02:00
sendSummaryToDiscord : ( id , webhookUrl ) => invoke ( 'send_summary_to_discord' , {
request : {
id ,
webhookUrl
}
} ) ,
2026-05-04 10:13:03 +02:00
translateSummary : ( id , lang , model , promptTemplate ) => invoke ( 'translate_summary' , {
2026-03-15 14:51:29 +01:00
request : {
id ,
lang ,
2026-05-04 10:13:03 +02:00
model : model || null ,
promptTemplate : promptTemplate || null
2026-03-15 14:51:29 +01:00
}
} ) ,
onSummarizeProgress : ( callback ) => listen ( 'summarize-progress' , ( event ) => {
callback ( String ( event . payload || '' ) ) ;
2026-05-04 10:13:03 +02:00
} ) ,
onOpenSettings : ( callback ) => listen ( 'open-settings' , callback )
2026-03-15 14:51:29 +01:00
} ;
window . addEventListener ( 'DOMContentLoaded' , async ( ) => {
const form = document . getElementById ( 'summarize-form' ) ;
const urlInput = document . getElementById ( 'url-input' ) ;
const whisperCheckbox = document . getElementById ( 'whisper-checkbox' ) ;
const summariesContainer = document . getElementById ( 'summaries-container' ) ;
const loadingIndicator = document . getElementById ( 'loading' ) ;
const modelSelect = document . getElementById ( 'model-select' ) ;
const paginationTop = document . getElementById ( 'pagination-top' ) ;
const paginationBottom = document . getElementById ( 'pagination-bottom' ) ;
const summarizeButton = form . querySelector ( 'button[type="submit"]' ) ;
const autoTranslateCheckbox = document . getElementById ( 'autotranslate-checkbox' ) ;
2026-05-04 10:13:03 +02:00
const settingsDialog = document . getElementById ( 'settings-dialog' ) ;
const settingsPanel = settingsDialog . querySelector ( '.settings-panel' ) ;
const settingsCloseButton = document . getElementById ( 'settings-close-button' ) ;
const masterPromptTextarea = document . getElementById ( 'master-prompt-textarea' ) ;
const resetMasterPromptButton = document . getElementById ( 'reset-master-prompt-button' ) ;
2026-05-04 10:33:38 +02:00
const discordWebhookInput = document . getElementById ( 'discord-webhook-url-input' ) ;
2026-05-04 10:13:03 +02:00
const translationPromptDeTextarea = document . getElementById ( 'translation-prompt-de-textarea' ) ;
const translationPromptJpTextarea = document . getElementById ( 'translation-prompt-jp-textarea' ) ;
const resetTranslationPromptDeButton = document . getElementById ( 'reset-translation-prompt-de-button' ) ;
const resetTranslationPromptJpButton = document . getElementById ( 'reset-translation-prompt-jp-button' ) ;
2026-03-15 14:51:29 +01:00
let fullSummaries = [ ] ;
let currentPage = 1 ;
const PAGE _SIZE = 20 ;
let isLoading = false ;
let entryUiState = { } ;
function setLoadingMessage ( message ) {
if ( ! isLoading ) {
return ;
}
loadingIndicator . style . display = 'inline' ;
loadingIndicator . textContent = message ;
}
2026-05-04 10:13:03 +02:00
function getMasterPrompt ( ) {
const savedPrompt = localStorage . getItem ( 'masterPrompt' ) ;
if ( savedPrompt && savedPrompt . trim ( ) ) {
return savedPrompt ;
}
return DEFAULT _MASTER _PROMPT ;
}
function getTranslationPrompt ( lang ) {
const savedPrompt = localStorage . getItem ( ` translationPrompt. ${ lang } ` ) ;
if ( savedPrompt && savedPrompt . trim ( ) ) {
return savedPrompt ;
}
return DEFAULT _TRANSLATION _PROMPTS [ lang ] ;
}
2026-05-04 10:33:38 +02:00
function getDiscordWebhookUrl ( ) {
return ( localStorage . getItem ( 'discordWebhookUrl' ) || '' ) . trim ( ) ;
}
2026-05-04 10:13:03 +02:00
function syncSettingsFields ( ) {
whisperCheckbox . checked = localStorage . getItem ( 'useWhisper' ) === '0' ? false : true ;
autoTranslateCheckbox . checked = localStorage . getItem ( 'autoTranslate' ) === '1' ? true : false ;
2026-05-04 10:33:38 +02:00
discordWebhookInput . value = getDiscordWebhookUrl ( ) ;
2026-05-04 10:13:03 +02:00
masterPromptTextarea . value = getMasterPrompt ( ) ;
translationPromptDeTextarea . value = getTranslationPrompt ( 'de' ) ;
translationPromptJpTextarea . value = getTranslationPrompt ( 'jp' ) ;
}
function openSettings ( ) {
syncSettingsFields ( ) ;
settingsDialog . hidden = false ;
2026-05-04 10:33:38 +02:00
discordWebhookInput . focus ( ) ;
2026-05-04 10:13:03 +02:00
}
function closeSettings ( ) {
settingsDialog . hidden = true ;
}
2026-03-15 14:51:29 +01:00
whisperCheckbox . checked = localStorage . getItem ( 'useWhisper' ) === '0' ? false : true ;
autoTranslateCheckbox . checked = localStorage . getItem ( 'autoTranslate' ) === '1' ? true : false ;
2026-05-04 10:33:38 +02:00
discordWebhookInput . value = getDiscordWebhookUrl ( ) ;
2026-05-04 10:13:03 +02:00
masterPromptTextarea . value = getMasterPrompt ( ) ;
translationPromptDeTextarea . value = getTranslationPrompt ( 'de' ) ;
translationPromptJpTextarea . value = getTranslationPrompt ( 'jp' ) ;
2026-03-15 14:51:29 +01:00
whisperCheckbox . addEventListener ( 'change' , ( ) => {
localStorage . setItem ( 'useWhisper' , whisperCheckbox . checked ? '1' : '0' ) ;
} ) ;
autoTranslateCheckbox . addEventListener ( 'change' , ( ) => {
localStorage . setItem ( 'autoTranslate' , autoTranslateCheckbox . checked ? '1' : '0' ) ;
} ) ;
2026-05-04 10:33:38 +02:00
discordWebhookInput . addEventListener ( 'input' , ( ) => {
const webhookUrl = discordWebhookInput . value . trim ( ) ;
if ( webhookUrl ) {
localStorage . setItem ( 'discordWebhookUrl' , webhookUrl ) ;
} else {
localStorage . removeItem ( 'discordWebhookUrl' ) ;
}
} ) ;
2026-05-04 10:13:03 +02:00
masterPromptTextarea . addEventListener ( 'input' , ( ) => {
localStorage . setItem ( 'masterPrompt' , masterPromptTextarea . value ) ;
} ) ;
translationPromptDeTextarea . addEventListener ( 'input' , ( ) => {
localStorage . setItem ( 'translationPrompt.de' , translationPromptDeTextarea . value ) ;
} ) ;
translationPromptJpTextarea . addEventListener ( 'input' , ( ) => {
localStorage . setItem ( 'translationPrompt.jp' , translationPromptJpTextarea . value ) ;
} ) ;
resetMasterPromptButton . addEventListener ( 'click' , ( ) => {
masterPromptTextarea . value = DEFAULT _MASTER _PROMPT ;
localStorage . setItem ( 'masterPrompt' , DEFAULT _MASTER _PROMPT ) ;
masterPromptTextarea . focus ( ) ;
} ) ;
resetTranslationPromptDeButton . addEventListener ( 'click' , ( ) => {
translationPromptDeTextarea . value = DEFAULT _TRANSLATION _PROMPTS . de ;
localStorage . setItem ( 'translationPrompt.de' , DEFAULT _TRANSLATION _PROMPTS . de ) ;
translationPromptDeTextarea . focus ( ) ;
} ) ;
resetTranslationPromptJpButton . addEventListener ( 'click' , ( ) => {
translationPromptJpTextarea . value = DEFAULT _TRANSLATION _PROMPTS . jp ;
localStorage . setItem ( 'translationPrompt.jp' , DEFAULT _TRANSLATION _PROMPTS . jp ) ;
translationPromptJpTextarea . focus ( ) ;
} ) ;
settingsCloseButton . addEventListener ( 'click' , closeSettings ) ;
settingsDialog . addEventListener ( 'click' , ( event ) => {
if ( ! settingsPanel . contains ( event . target ) ) {
closeSettings ( ) ;
}
} ) ;
window . addEventListener ( 'keydown' , ( event ) => {
if ( event . key === 'Escape' && ! settingsDialog . hidden ) {
closeSettings ( ) ;
}
} ) ;
2026-03-15 14:51:29 +01:00
function renderSummaries ( list ) {
summariesContainer . innerHTML = '' ;
const renderedIds = new Set ( ) ;
list . forEach ( item => {
renderedIds . add ( item . id ) ;
if ( ! entryUiState [ item . id ] ) {
entryUiState [ item . id ] = { expanded : false , lang : 'en' } ;
}
let { expanded , lang } = entryUiState [ item . id ] ;
const entry = document . createElement ( 'div' ) ;
entry . classList . add ( 'entry' ) ;
entry . style . overflow = 'hidden' ;
2026-05-04 10:33:50 +02:00
const entryActions = document . createElement ( 'div' ) ;
entryActions . classList . add ( 'entry-actions' ) ;
const uploadButton = document . createElement ( 'button' ) ;
uploadButton . type = 'button' ;
uploadButton . innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 16V4"></path><path d="m7 9 5-5 5 5"></path><path d="M20 16v4H4v-4"></path></svg>' ;
uploadButton . classList . add ( 'entry-icon-button' , 'upload-entry-button' ) ;
uploadButton . title = 'Send to Discord' ;
uploadButton . setAttribute ( 'aria-label' , 'Send to Discord' ) ;
uploadButton . disabled = isLoading ;
uploadButton . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
if ( isLoading ) {
return ;
}
const webhookUrl = getDiscordWebhookUrl ( ) ;
if ( ! webhookUrl ) {
alert ( 'Add a Discord Webhook URL in Settings first.' ) ;
openSettings ( ) ;
return ;
}
isLoading = true ;
summarizeButton . disabled = true ;
setLoadingMessage ( 'Sending to Discord...' ) ;
setActionLinksDisabled ( true ) ;
window . api . sendSummaryToDiscord ( item . id , webhookUrl )
. catch ( err => {
alert ( 'Error sending to Discord: ' + err . message ) ;
} )
. finally ( ( ) => {
loadingIndicator . style . display = 'none' ;
loadingIndicator . textContent = 'Loading...' ;
summarizeButton . disabled = false ;
isLoading = false ;
setActionLinksDisabled ( false ) ;
} ) ;
} ) ;
2026-03-15 14:51:29 +01:00
const deleteButton = document . createElement ( 'button' ) ;
deleteButton . type = 'button' ;
deleteButton . innerHTML = '×' ;
2026-05-04 10:33:50 +02:00
deleteButton . classList . add ( 'entry-icon-button' , 'delete-entry-button' ) ;
deleteButton . title = 'Delete entry' ;
deleteButton . setAttribute ( 'aria-label' , 'Delete entry' ) ;
2026-03-15 14:51:29 +01:00
deleteButton . disabled = isLoading ;
deleteButton . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
if ( isLoading ) {
return ;
}
if ( typeof confirmDialog !== 'function' ) {
alert ( 'Delete confirmation is unavailable.' ) ;
return ;
}
confirmDialog ( 'Are you sure you want to delete this entry?' , {
title : 'Delete entry' ,
kind : 'warning'
} ) . then ( ( confirmed ) => {
if ( ! confirmed ) {
return ;
}
window . api . deleteSummary ( item . id )
. then ( ( ) => {
delete entryUiState [ item . id ] ;
return window . api . getSummaries ( ) . then ( setSummaries ) ;
} )
. catch ( err => {
alert ( 'Error deleting summary: ' + err . message ) ;
} ) ;
} ) ;
} ) ;
const left = document . createElement ( 'div' ) ;
left . classList . add ( 'left' ) ;
if ( item . thumbnail ) {
const img = document . createElement ( 'img' ) ;
img . src = toWebviewFileUrl ( item . thumbnail ) ;
img . alt = item . video _name ;
img . classList . add ( 'thumbnail' ) ;
if ( item . url ) {
img . style . cursor = 'pointer' ;
img . title = 'Open video' ;
img . addEventListener ( 'click' , ( e ) => {
e . stopPropagation ( ) ;
window . api . openExternal ( item . url ) ;
} ) ;
}
left . appendChild ( img ) ;
}
const langSwitcher = document . createElement ( 'span' ) ;
langSwitcher . style . display = 'flex' ;
langSwitcher . style . gap = '6px' ;
langSwitcher . style . marginTop = '8px' ;
langSwitcher . style . marginBottom = '2px' ;
const summaryFields = {
en : item . summary _en ,
de : item . summary _de ,
jp : item . summary _jp
} ;
[ 'en' , 'de' , 'jp' ] . forEach ( thisLang => {
const btn = document . createElement ( 'button' ) ;
btn . type = 'button' ;
btn . textContent = thisLang . toUpperCase ( ) ;
btn . style . fontSize = '12px' ;
btn . style . padding = '2px 8px' ;
btn . style . borderRadius = '5px' ;
btn . style . border = '1px solid #eee' ;
btn . style . background = ( thisLang === lang ) ? '#9f1239' : '#fff1f2' ;
btn . style . color = ( thisLang === lang ) ? '#fff' : '#9f1239' ;
btn . disabled = isLoading ;
btn . addEventListener ( 'click' , ( ) => {
lang = thisLang ;
entryUiState [ item . id ] . lang = lang ;
renderSummaryContent ( ) ;
Array . from ( langSwitcher . children ) . forEach ( ( button , index ) => {
const language = [ 'en' , 'de' , 'jp' ] [ index ] ;
button . style . background = ( language === lang ) ? '#9f1239' : '#fff1f2' ;
button . style . color = ( language === lang ) ? '#fff' : '#9f1239' ;
} ) ;
} ) ;
langSwitcher . appendChild ( btn ) ;
} ) ;
left . appendChild ( langSwitcher ) ;
const middle = document . createElement ( 'div' ) ;
middle . classList . add ( 'middle' ) ;
const headline = document . createElement ( 'div' ) ;
headline . style . display = 'flex' ;
headline . style . alignItems = 'center' ;
headline . style . justifyContent = 'space-between' ;
headline . style . gap = '12px' ;
const headlineMain = document . createElement ( 'div' ) ;
headlineMain . style . display = 'flex' ;
headlineMain . style . alignItems = 'center' ;
headlineMain . style . minWidth = '0' ;
const titleEl = document . createElement ( 'strong' ) ;
titleEl . style . display = 'block' ;
titleEl . style . fontSize = '16px' ;
titleEl . style . cursor = 'default' ;
titleEl . style . marginLeft = '0' ;
titleEl . textContent = item . video _name ;
const arrow = document . createElement ( 'span' ) ;
arrow . textContent = expanded ? '▼' : '▶' ;
arrow . style . marginRight = '8px' ;
arrow . style . marginLeft = '0' ;
arrow . style . fontSize = '18px' ;
arrow . style . userSelect = 'none' ;
arrow . style . transition = 'transform 0.15s' ;
headlineMain . appendChild ( arrow ) ;
headlineMain . appendChild ( titleEl ) ;
headline . appendChild ( headlineMain ) ;
2026-05-04 10:33:50 +02:00
entryActions . appendChild ( uploadButton ) ;
entryActions . appendChild ( deleteButton ) ;
headline . appendChild ( entryActions ) ;
2026-03-15 14:51:29 +01:00
const channelEl = document . createElement ( 'span' ) ;
channelEl . style . fontSize = '14px' ;
channelEl . style . opacity = '0.8' ;
channelEl . style . marginBottom = '12px' ;
channelEl . textContent = item . channel || '' ;
channelEl . style . display = 'block' ;
channelEl . style . marginTop = '2px' ;
middle . appendChild ( headline ) ;
middle . appendChild ( channelEl ) ;
const summaryHTML = document . createElement ( 'div' ) ;
summaryHTML . classList . add ( 'summary' ) ;
summaryHTML . style . display = '-webkit-box' ;
summaryHTML . style . webkitBoxOrient = 'vertical' ;
summaryHTML . style . overflow = 'hidden' ;
summaryHTML . style . transition = 'max-height 0.2s' ;
function renderSummaryContent ( ) {
const text = summaryFields [ lang ] ;
summaryHTML . innerHTML = '' ;
if ( text && text . trim ( ) ) {
summaryHTML . innerHTML = markdownToHTML ( text ) ;
} else {
const missingMsg = document . createElement ( 'span' ) ;
missingMsg . textContent = (
lang === 'de' ? 'German not available. ' :
lang === 'jp' ? 'Japanese not available. ' :
'Not available. '
) ;
summaryHTML . appendChild ( missingMsg ) ;
}
if ( ! expanded ) {
summaryHTML . style . webkitLineClamp = '2' ;
summaryHTML . style . maxHeight = '2.8em' ;
} else {
summaryHTML . style . webkitLineClamp = '' ;
summaryHTML . style . maxHeight = '' ;
}
}
middle . appendChild ( summaryHTML ) ;
entry . appendChild ( left ) ;
entry . appendChild ( middle ) ;
summariesContainer . appendChild ( entry ) ;
function applyCollapsedStyle ( ) {
if ( ! expanded ) {
entry . classList . add ( 'collapsed' ) ;
arrow . textContent = '▶' ;
} else {
entry . classList . remove ( 'collapsed' ) ;
arrow . textContent = '▼' ;
}
renderSummaryContent ( ) ;
}
applyCollapsedStyle ( ) ;
middle . addEventListener ( 'click' , ( ) => {
if ( ! expanded ) {
expanded = true ;
entryUiState [ item . id ] . expanded = true ;
applyCollapsedStyle ( ) ;
}
} ) ;
headline . addEventListener ( 'click' , ( e ) => {
if ( expanded ) {
expanded = false ;
entryUiState [ item . id ] . expanded = false ;
applyCollapsedStyle ( ) ;
e . stopPropagation ( ) ;
}
} ) ;
} ) ;
Object . keys ( entryUiState ) . forEach ( id => {
if ( ! renderedIds . has ( Number ( id ) ) ) {
delete entryUiState [ id ] ;
}
} ) ;
setActionLinksDisabled ( isLoading ) ;
}
function markdownToHTML ( text ) {
text = text . replace ( /<\/think(?:ing)?>[^\S\n]*\n+[^\S\n]*/gi , '</think>' ) ;
text = text . replace (
/(^|\n)\s*<think>[\s\S]*?<\/think(?:ing)?>\s*(\n\s*\n)?/gi ,
( _ , lead ) => ( lead ? '\n' : '' )
) ;
let tmp = text . replace ( /\r\n/g , '\n' ) . replace ( /\r/g , '\n' ) ;
tmp = tmp . replace (
/(^|\n)\s*<think>[\s\S]*?<\/think(?:ing)?>\s*(?=\n|$)/gi ,
( _ , lead ) => ( lead ? '\n' : '' )
) ;
const codeblocks = [ ] ;
const placeholder = idx => ` @@CODEBLOCK ${ idx } @@ ` ;
tmp = tmp . replace ( /```([\s\S]*?)```/g , ( _ , code ) => {
codeblocks . push ( code ) ;
return placeholder ( codeblocks . length - 1 ) ;
} ) ;
let escaped = tmp
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' ) ;
escaped = escaped
. replace ( /^#### (.+)$/gm , '<h4>$1</h4>' )
. replace ( /^### (.+)$/gm , '<h3>$1</h3>' )
. replace ( /^## (.+)$/gm , '<h2>$1</h2>' )
. replace ( /^# (.+)$/gm , '<h1>$1</h1>' ) ;
2026-05-04 10:13:03 +02:00
escaped = escaped . replace ( /^---$/gm , '<hr>' ) ;
2026-03-15 14:51:29 +01:00
escaped = escaped . replace (
/(^|\n)([ \t]*\* .+(?:\n[ \t]*\* .+)*)/g ,
( _ , lead , listBlock ) => {
const items = listBlock
. split ( /\n/ )
. map ( line => line . replace ( /^[ \t]*\*\s+/ , '' ) . trim ( ) )
. map ( item => ` <li> ${ item } </li> ` )
. join ( '' ) ;
return ` ${ lead } <ul> ${ items } </ul> ` ;
}
) ;
let html = escaped
. replace ( /\*\*(.+?)\*\*/g , '<b>$1</b>' )
. replace ( /(?<!\*)\*(.+?)\*(?!\*)/g , '<i>$1</i>' )
. replace ( /`(.+?)`/g , '<code>$1</code>' ) ;
html = html . replace ( /@@CODEBLOCK(\d+)@@/g , ( _ , idx ) => {
const code = codeblocks [ Number ( idx ) ] ;
return ` <pre><code> ${ code } </code></pre> ` ;
} ) ;
html = html . replace ( /\n*(<h[1-3]>.*?<\/h[1-3]>)\n*/g , '$1\n' ) ;
html = html . replace ( /\n/g , '<br>' ) ;
html = html
. replace ( /<br>\s*(<h[1-3]>)/g , '$1' )
. replace ( /(<\/h[1-3]>)\s*<br>/g , '$1' ) ;
return html ;
}
function setActionLinksDisabled ( disabled ) {
2026-05-04 10:34:05 +02:00
document . querySelectorAll ( '.entry-icon-button' ) . forEach ( button => {
2026-03-15 14:51:29 +01:00
if ( disabled ) {
button . disabled = true ;
button . style . opacity = '0.5' ;
} else {
button . disabled = false ;
button . style . opacity = '' ;
}
} ) ;
document . querySelectorAll ( '.left button' ) . forEach ( btn => {
btn . disabled = disabled ;
btn . style . opacity = disabled ? '0.5' : '' ;
} ) ;
}
function updatePaginationControls ( ) {
if ( ! fullSummaries || fullSummaries . length <= PAGE _SIZE ) {
paginationTop . style . display = 'none' ;
paginationBottom . style . display = 'none' ;
return ;
}
paginationTop . style . display = 'flex' ;
paginationBottom . style . display = 'flex' ;
const totalPages = Math . ceil ( fullSummaries . length / PAGE _SIZE ) ;
const buildNav = ( container ) => {
container . innerHTML = '' ;
const prevBtn = document . createElement ( 'button' ) ;
prevBtn . textContent = '«' ;
prevBtn . disabled = currentPage === 1 ;
prevBtn . addEventListener ( 'click' , ( ) => {
if ( currentPage > 1 ) {
showPage ( currentPage - 1 ) ;
updatePaginationControls ( ) ;
}
} ) ;
container . appendChild ( prevBtn ) ;
for ( let i = 1 ; i <= totalPages ; i += 1 ) {
const btn = document . createElement ( 'button' ) ;
btn . textContent = i ;
if ( i === currentPage ) {
btn . classList . add ( 'active' ) ;
}
btn . addEventListener ( 'click' , ( ) => {
showPage ( i ) ;
updatePaginationControls ( ) ;
} ) ;
container . appendChild ( btn ) ;
}
const nextBtn = document . createElement ( 'button' ) ;
nextBtn . textContent = '»' ;
nextBtn . disabled = currentPage === totalPages ;
nextBtn . addEventListener ( 'click' , ( ) => {
if ( currentPage < totalPages ) {
showPage ( currentPage + 1 ) ;
updatePaginationControls ( ) ;
}
} ) ;
container . appendChild ( nextBtn ) ;
} ;
buildNav ( paginationTop ) ;
buildNav ( paginationBottom ) ;
}
function showPage ( page ) {
const totalPages = Math . ceil ( fullSummaries . length / PAGE _SIZE ) ;
currentPage = Math . max ( 1 , Math . min ( page , totalPages || 1 ) ) ;
const start = ( currentPage - 1 ) * PAGE _SIZE ;
const end = start + PAGE _SIZE ;
renderSummaries ( fullSummaries . slice ( start , end ) ) ;
}
function setSummaries ( list ) {
fullSummaries = list || [ ] ;
const totalPages = Math . ceil ( fullSummaries . length / PAGE _SIZE ) ;
if ( currentPage > totalPages ) {
currentPage = Math . max ( 1 , totalPages ) ;
}
showPage ( currentPage ) ;
updatePaginationControls ( ) ;
}
try {
const models = await window . api . getModels ( ) ;
modelSelect . innerHTML = '' ;
const hasMistral = Array . isArray ( models ) && models . includes ( 'mistral:latest' ) ;
const placeholder = document . createElement ( 'option' ) ;
placeholder . disabled = true ;
placeholder . value = '' ;
placeholder . innerText = 'Select model' ;
modelSelect . appendChild ( placeholder ) ;
if ( Array . isArray ( models ) ) {
models . forEach ( name => {
const option = document . createElement ( 'option' ) ;
option . value = name ;
option . innerText = name ;
modelSelect . appendChild ( option ) ;
} ) ;
}
const saved = localStorage . getItem ( 'selectedModel' ) ;
let toSelect = '' ;
if ( saved && models . includes ( saved ) ) {
toSelect = saved ;
} else if ( hasMistral ) {
toSelect = 'mistral:latest' ;
}
if ( toSelect ) {
modelSelect . value = toSelect ;
placeholder . selected = false ;
} else {
placeholder . selected = true ;
}
} catch ( err ) {
console . error ( 'Error loading models:' , err ) ;
modelSelect . innerHTML = '' ;
const placeholder = document . createElement ( 'option' ) ;
placeholder . disabled = true ;
placeholder . selected = true ;
placeholder . value = '' ;
placeholder . innerText = 'Select model' ;
modelSelect . appendChild ( placeholder ) ;
}
modelSelect . addEventListener ( 'change' , ( ) => {
localStorage . setItem ( 'selectedModel' , modelSelect . value ) ;
} ) ;
window . api . getSummaries ( ) . then ( setSummaries ) . catch ( console . error ) ;
form . addEventListener ( 'submit' , ( e ) => {
e . preventDefault ( ) ;
const url = urlInput . value . trim ( ) ;
const useWhisper = whisperCheckbox . checked ;
const autoTranslate = autoTranslateCheckbox . checked ;
if ( ! url || isLoading ) {
return ;
}
isLoading = true ;
summarizeButton . disabled = true ;
setLoadingMessage ( 'Summarizing…' ) ;
setActionLinksDisabled ( true ) ;
const selectedModel = modelSelect . value ;
2026-05-04 10:13:03 +02:00
window . api . summarizeVideo ( url , useWhisper , selectedModel , getMasterPrompt ( ) )
2026-03-15 14:51:29 +01:00
. then ( ( newEntry ) => {
if ( ! newEntry || ! newEntry . id ) {
return window . api . getSummaries ( ) . then ( setSummaries ) ;
}
entryUiState [ newEntry . id ] = { expanded : true , lang : 'en' } ;
if ( ! autoTranslate ) {
return window . api . getSummaries ( ) . then ( setSummaries ) ;
}
let translationsOk = true ;
setLoadingMessage ( 'Translating to German (DE)…' ) ;
2026-05-04 10:13:03 +02:00
return window . api . translateSummary ( newEntry . id , 'de' , selectedModel , getTranslationPrompt ( 'de' ) )
2026-03-15 14:51:29 +01:00
. then ( ( ) => {
setLoadingMessage ( 'Translating to Japanese (JP)…' ) ;
2026-05-04 10:13:03 +02:00
return window . api . translateSummary ( newEntry . id , 'jp' , selectedModel , getTranslationPrompt ( 'jp' ) ) ;
2026-03-15 14:51:29 +01:00
} )
. catch ( err => {
translationsOk = false ;
alert ( 'Error translating summary: ' + err . message ) ;
} )
. then ( ( ) => {
entryUiState [ newEntry . id ] = {
expanded : true ,
lang : translationsOk ? 'jp' : 'en'
} ;
return window . api . getSummaries ( ) . then ( setSummaries ) ;
} ) ;
} )
. catch ( err => {
alert ( 'Error summarizing video: ' + err . message ) ;
} )
. finally ( ( ) => {
loadingIndicator . style . display = 'none' ;
loadingIndicator . textContent = 'Loading…' ;
summarizeButton . disabled = false ;
isLoading = false ;
setActionLinksDisabled ( false ) ;
urlInput . value = '' ;
} ) ;
} ) ;
window . api . onSummarizeProgress ( line => {
if ( ! isLoading || ! line ) {
return ;
}
setLoadingMessage ( line ) ;
} ) ;
2026-05-04 10:13:03 +02:00
window . api . onOpenSettings ( openSettings ) ;
2026-03-15 14:51:29 +01:00
} ) ;