feat: WebUI: inline YouTube transcripts during chat flow

- Replace YouTube URLs with fetched transcript blocks
- Support multiple YouTube links inside one message
- Always render user message before processing response
- Show YouTube-specific loading indicator during transcript fetch
- Reset YouTube URL state after submission processing
- Upgrade SvelteKit, Svelte, Tailwind, TypeScript toolchain versions
- Bump pdfjs-dist and @napi-rs/canvas optional dependency versions
- Refresh esbuild binaries and Floating UI runtime versions
- Regenerate pnpm/npm lockfiles with updated transitive dependencies
This commit is contained in:
Kayvan Sylvan 2026-02-28 15:21:47 -08:00
parent c6789bedf7
commit 34bd2ece1e
4 changed files with 1020 additions and 1066 deletions

911
web/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -16,35 +16,35 @@
"format": "prettier --write ."
},
"devDependencies": {
"@eslint/js": "^9.27.0",
"@eslint/js": "^9.39.3",
"@skeletonlabs/skeleton": "^2.11.0",
"@skeletonlabs/tw-plugin": "^0.3.1",
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/kit": "^2.53.1",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^20.17.50",
"autoprefixer": "^10.4.21",
"@sveltejs/kit": "^2.53.4",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^20.19.35",
"autoprefixer": "^10.4.27",
"eslint-plugin-svelte": "^2.46.1",
"lucide-svelte": "^0.575.0",
"mdsvex": "^0.11.2",
"patch-package": "^8.0.1",
"pdf-to-markdown-core": "github:jzillmann/pdf-to-markdown#modularize",
"pdfjs-dist": "^5.4.449",
"postcss": "^8.5.3",
"pdfjs-dist": "^5.4.624",
"postcss": "^8.5.6",
"postcss-load-config": "^6.0.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"shiki": "^1.29.2",
"svelte": "^5.53.5",
"svelte": "^5.53.6",
"svelte-check": "^3.8.6",
"svelte-inview": "^4.0.4",
"svelte-reveal": "^1.1.0",
"svelte-youtube-embed": "^0.3.3",
"svelte-youtube-lite": "^0.6.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"tailwindcss": "^3.4.19",
"typescript": "^5.9.3",
"vite": "^5.4.21",
"vite-plugin-tailwind-purgecss": "^0.2.1"
},
@ -65,7 +65,7 @@
"@eslint/plugin-kit": ">=0.3.4"
},
"dependencies": {
"@floating-ui/dom": "^1.7.0",
"@floating-ui/dom": "^1.7.5",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"highlight.js": "^11.11.1",
@ -74,9 +74,9 @@
"rehype": "^13.0.2",
"rehype-external-links": "^3.0.0",
"rehype-unwrap-images": "^1.0.0",
"tailwind-merge": "^2.6.0",
"vfile-message": "^4.0.2",
"yaml": "^2.8.0",
"tailwind-merge": "^2.6.1",
"vfile-message": "^4.0.3",
"yaml": "^2.8.2",
"youtube-transcript": "^1.2.1"
},
"pnpm": {

File diff suppressed because it is too large Load diff

View file

@ -306,78 +306,21 @@ async function readFileContent(file: File): Promise<string> {
}
// Centralized language instruction logic in ChatService.ts; YouTube flow now passes plain transcript and system prompt
async function processYouTubeURL(input: string) {
console.log('\n=== YouTube Flow Start ===');
const originalLanguage = get(languageStore);
function extractYouTubeURLs(input: string): string[] {
const youtubePattern = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=[\w-]+(?:&[^\s]*)?|youtu\.be\/[\w-]+(?:\?[^\s]*)?)/gi;
return input.match(youtubePattern) || [];
}
try {
// Add processing message first
messageStore.update(messages => [...messages, {
role: 'system',
content: 'Processing YouTube video...',
format: 'loading'
}]);
async function replaceYouTubeURLsWithTranscripts(input: string): Promise<string> {
const urls = extractYouTubeURLs(input);
if (urls.length === 0) return input;
// Get transcript but don't display it
const { transcript } = await getTranscript(input);
// Pass plain transcript and system prompt; ChatService will handle language instruction
const stream = await chatService.streamChat(transcript, $systemPrompt);
await chatService.processStream(
stream,
(content, response) => {
messageStore.update(messages => {
const newMessages = [...messages];
// Replace the processing message with actual content
const loadingIndex = newMessages.findIndex(m => m.format === 'loading');
if (loadingIndex !== -1) {
newMessages.splice(loadingIndex, 1);
}
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage?.role === 'assistant') {
lastMessage.content += content;
lastMessage.format = response?.format;
} else {
newMessages.push({
role: 'assistant',
content,
format: response?.format
});
}
return newMessages;
});
},
(error) => {
messageStore.update(messages =>
messages.filter(m => m.format !== 'loading')
);
throw error;
}
);
// Handle Obsidian saving if needed
if ($obsidianSettings.saveToObsidian) {
let lastContent = '';
messageStore.subscribe(messages => {
const lastMessage = messages[messages.length - 1];
if (lastMessage?.role === 'assistant') {
lastContent = lastMessage.content;
}
})();
if (lastContent) await saveToObsidian(lastContent);
}
userInput = "";
uploadedFiles = [];
fileContents = [];
} catch (error) {
console.error('Error processing YouTube URL:', error);
messageStore.update(messages =>
messages.filter(m => m.format !== 'loading')
);
throw error;
let result = input;
for (const url of urls) {
const { transcript } = await getTranscript(url);
result = result.replace(url, `[YouTube Transcript]:\n${transcript}`);
}
return result;
}
async function handleSubmit() {
@ -385,46 +328,48 @@ async function readFileContent(file: File): Promise<string> {
try {
console.log('\n=== Submit Handler Start ===');
// Store the user input before any processing
const inputText = userInput.trim();
console.log('Captured user input:', inputText);
// Handle YouTube URLs with the existing flow
if (isYouTubeURL) {
console.log('2a. Starting YouTube flow');
await processYouTubeURL(inputText);
return;
}
// For regular text input, add the user message to the UI first
// Add the user message to the UI first
messageStore.update(messages => [...messages, {
role: 'user',
content: inputText
}]);
// Add loading indicator
messageStore.update(messages => [...messages, {
role: 'system',
content: 'Processing...',
content: isYouTubeURL ? 'Processing YouTube video...' : 'Processing...',
format: 'loading'
}]);
// Clear input fields
userInput = "";
const hadYouTubeURL = isYouTubeURL;
isYouTubeURL = false;
const filesForProcessing = [...uploadedFiles];
const contentsForProcessing = [...fileContents];
uploadedFiles = [];
fileContents = [];
fileButtonKey = !fileButtonKey;
// If the message contains YouTube URLs, replace them with transcripts
let processedText = inputText;
if (hadYouTubeURL) {
console.log('Replacing YouTube URLs with transcripts');
processedText = await replaceYouTubeURLsWithTranscripts(inputText);
}
// Prepare content with file attachments if any
const contentWithFiles = contentsForProcessing.length > 0
? `${inputText}\n\nFile Contents (${filesForProcessing.map(f => f.endsWith('.pdf') ? 'PDF' : 'Text').join(', ')}):\n${contentsForProcessing.join('\n\n---\n\n')}`
: inputText;
const contentWithFiles = contentsForProcessing.length > 0
? `${processedText}\n\nFile Contents (${filesForProcessing.map(f => f.endsWith('.pdf') ? 'PDF' : 'Text').join(', ')}):\n${contentsForProcessing.join('\n\n---\n\n')}`
: processedText;
// Get the enhanced prompt
const enhancedPrompt = contentsForProcessing.length > 0
const enhancedPrompt = contentsForProcessing.length > 0
? `${$systemPrompt}\nAnalyze and process the provided content according to these instructions.`
: $systemPrompt;