mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-09 14:28:01 -05:00
House Keeping: Fixing Indentation
This commit is contained in:
@@ -4,52 +4,25 @@
|
||||
import ModelConfig from "./ModelConfig.svelte";
|
||||
import Models from "./Models.svelte";
|
||||
import Patterns from "./Patterns.svelte";
|
||||
//import NoteDrawer from '$lib/components/ui/noteDrawer/NoteDrawer.svelte';
|
||||
//import { getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
//import { Button } from '$lib/components/ui/button';
|
||||
//import { page } from '$app/stores';
|
||||
//import { beforeNavigate } from '$app/navigation';
|
||||
//
|
||||
//const drawerStore = getDrawerStore();
|
||||
//function openDrawer() {
|
||||
// drawerStore.open({});
|
||||
//}
|
||||
//
|
||||
//beforeNavigate(() => {
|
||||
// drawerStore.close();
|
||||
//});
|
||||
//
|
||||
//$: isVisible = $page.url.pathname.startsWith('/chat');
|
||||
</script>
|
||||
|
||||
<div content="width=device-width, height=device-height, initial-scale=1.0">
|
||||
<div class="h-screen overflow-auto">
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex-1 overflow-hidden mx-auto p-2">
|
||||
<!-- <div class="flex flex-col columns-3 m-2 p-1 h-screen"> -->
|
||||
<div class="h-full flex gap-2">
|
||||
<aside class="w-1/5 overflow-y-auto">
|
||||
<!-- <div class="space-y-2"> -->
|
||||
<!-- <div class="flex flex-col gap-2"> -->
|
||||
<Patterns />
|
||||
<Models />
|
||||
<ModelConfig />
|
||||
<!-- </div> -->
|
||||
|
||||
<!-- </div> -->
|
||||
</aside>
|
||||
<div class="w-1/2">
|
||||
<!-- <div class="flex flex-col space-y-4 order-2 lg:order-2 h-screen"> -->
|
||||
<ChatInput />
|
||||
</div>
|
||||
<br>
|
||||
<div class="w-1/2">
|
||||
<!-- <div class="flex flex-col rounded-lg order-1 lg:order-3 h-screen w-full"> -->
|
||||
<ChatMessages />
|
||||
</div>
|
||||
<div class="mt-2 flex h-screen w-full overflow-hidden">
|
||||
<div class="flex gap-4 p-2 w-full">
|
||||
<aside class="w-1/5">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Patterns />
|
||||
<Models />
|
||||
<ModelConfig />
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
</aside>
|
||||
|
||||
<div class="w-1/2">
|
||||
<ChatInput />
|
||||
</div>
|
||||
|
||||
<div class="w-1/2">
|
||||
<ChatMessages />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
32
web/src/lib/components/chat/Chat.svelte.bak
Normal file
32
web/src/lib/components/chat/Chat.svelte.bak
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
<div class="flex flex-col h-screen" style="width=device-width, height=device-height, initial-scale=2.0">
|
||||
<div class="h-screen overflow-auto">
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex-1 overflow-hidden mx-auto p-2">
|
||||
<!-- <div class="flex flex-col columns-3 m-2 p-1 h-screen"> -->
|
||||
<div class="h-full flex gap-2">
|
||||
<aside class="w-1/5 overflow-y-auto">
|
||||
<!-- <div class="space-y-2"> -->
|
||||
<!-- <div class="flex flex-col gap-2"> -->
|
||||
<Patterns />
|
||||
<Models />
|
||||
<ModelConfig />
|
||||
<!-- </div> -->
|
||||
|
||||
<!-- </div> -->
|
||||
</aside>
|
||||
<div class="w-1/2">
|
||||
<!-- <div class="flex flex-col space-y-4 order-2 lg:order-2 h-screen"> -->
|
||||
<ChatInput />
|
||||
</div>
|
||||
<br>
|
||||
<div class="w-1/2">
|
||||
<!-- <div class="flex flex-col rounded-lg order-1 lg:order-3 h-screen w-full"> -->
|
||||
<ChatMessages />
|
||||
</div>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,48 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { sendMessage, messageStore } from '$lib/store/chat';
|
||||
import { systemPrompt } from '$lib/store/pattern-store';
|
||||
import { getToastStore } from '@skeletonlabs/skeleton';
|
||||
import { FileButton } from '@skeletonlabs/skeleton';
|
||||
import { Paperclip, Send } from 'lucide-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { sendMessage, messageStore } from '$lib/store/chat-store';
|
||||
import { systemPrompt } from '$lib/store/pattern-store';
|
||||
import { getToastStore } from '@skeletonlabs/skeleton';
|
||||
import { FileButton } from '@skeletonlabs/skeleton';
|
||||
import { Paperclip, Send, FileCheck } from 'lucide-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let userInput = "";
|
||||
let files: FileList;
|
||||
const toastStore = getToastStore();
|
||||
let userInput = "";
|
||||
//let files: FileList;
|
||||
const toastStore = getToastStore();
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!userInput.trim()) return;
|
||||
let files: File[] = [];
|
||||
let uploadedFiles: string[] = [];
|
||||
let fileContents: string[] = [];
|
||||
let isProcessingFiles = false;
|
||||
|
||||
try {
|
||||
const trimmedInput = userInput.trim();
|
||||
const trimmedSystemPrompt = $systemPrompt.trim();
|
||||
|
||||
// Clear input before sending to improve perceived performance
|
||||
userInput = "";
|
||||
|
||||
await sendMessage(trimmedSystemPrompt + '\n' + trimmedInput);
|
||||
} catch (error) {
|
||||
console.error('Chat submission error:', error);
|
||||
toastStore.trigger({
|
||||
message: 'Failed to send message. Please try again.',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
}
|
||||
async function handleFileUpload(e: Event) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
if (uploadedFiles.length >= 5 || (uploadedFiles.length + files.length) > 5) {
|
||||
toastStore.error('Maximum 5 files allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
isProcessingFiles = true;
|
||||
try {
|
||||
for (let i = 0; i < files.length && uploadedFiles.length < 5; i++) {
|
||||
const file = files[i];
|
||||
const content = await readFileContent(file);
|
||||
fileContents.push(content);
|
||||
uploadedFiles = [...uploadedFiles, file.name];
|
||||
}
|
||||
} catch (error) {
|
||||
toastStore.error('Error processing files: ' + error.message);
|
||||
} finally {
|
||||
isProcessingFiles = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
console.log('ChatInput mounted, current system prompt:', $systemPrompt);
|
||||
function readFileContent(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => resolve(e.target?.result as string);
|
||||
reader.onerror = (e) => reject(new Error('Failed to read file'));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!userInput.trim()) return;
|
||||
|
||||
try {
|
||||
let finalContent = "";
|
||||
if (fileContents.length > 0) {
|
||||
finalContent += '\n\nFile Contents:\n' + fileContents.map((content, index) =>
|
||||
`[${uploadedFiles[index]}]:\n${content}`
|
||||
).join('\n\n');
|
||||
}
|
||||
|
||||
const trimmedInput = userInput.trim() + '\n' + (finalContent || '');
|
||||
const trimmedSystemPrompt = $systemPrompt.trim();
|
||||
let messageHistory = JSON.stringify($messageStore);
|
||||
|
||||
$systemPrompt = "";
|
||||
userInput = "";
|
||||
uploadedFiles = [];
|
||||
fileContents = [];
|
||||
|
||||
// Place the messageHistory in the sendMessage(``) function to send the message history
|
||||
// This is a WIP and temporarily disabled.
|
||||
await sendMessage(`${trimmedSystemPrompt}\n${trimmedInput}\n${finalContent}`);
|
||||
} catch (error) {
|
||||
console.error('Chat submission error:', error);
|
||||
toastStore.trigger({
|
||||
message: 'Failed to send message. Please try again.',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
console.log('ChatInput mounted, current system prompt:', $systemPrompt);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="h-full">
|
||||
@@ -68,17 +117,30 @@
|
||||
|
||||
<FileButton
|
||||
name="file-upload"
|
||||
button="btn btn-sm variant-soft-surface"
|
||||
bind:files={files}
|
||||
on:change={(e) => {
|
||||
// Workin on the file selection
|
||||
// Check out `https://www.skeleton.dev/components/file-buttons` for more info
|
||||
// Check 24-12-08 for half-baked implementation
|
||||
}}
|
||||
button="btn variant-default"
|
||||
bind:files
|
||||
on:change={handleFileUpload}
|
||||
disabled={isProcessingFiles || uploadedFiles.length >= 5}
|
||||
>
|
||||
<Paperclip class="w-4" />
|
||||
{#if uploadedFiles.length > 0}
|
||||
<FileCheck class="w-4 h-4" />
|
||||
{:else}
|
||||
<Paperclip class="w-4 h-4" />
|
||||
{/if}
|
||||
</FileButton>
|
||||
<Button type="button" name="submit" variant="secondary" on:click={handleSubmit}>
|
||||
{#if uploadedFiles.length > 0}
|
||||
<span class="text-sm text-gray-500 space-x-2">
|
||||
{uploadedFiles.length} file{uploadedFiles.length > 1 ? 's' : ''} attached
|
||||
</span>
|
||||
{/if}
|
||||
<br>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
name="send"
|
||||
on:click={handleSubmit}
|
||||
disabled={isProcessingFiles || !userInput.trim()}
|
||||
>
|
||||
<Send class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -87,7 +149,7 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.flex-col {
|
||||
min-height: 0;
|
||||
}
|
||||
.flex-col {
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { chatState, errorStore, streamingStore } from '$lib/store/chat';
|
||||
import { afterUpdate } from 'svelte';
|
||||
import { toastStore } from '$lib/store/toast-store';
|
||||
import { marked } from 'marked';
|
||||
import SessionManager from './SessionManager.svelte';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import { chatState, errorStore, streamingStore } from '$lib/store/chat-store';
|
||||
import { afterUpdate } from 'svelte';
|
||||
import { toastStore } from '$lib/store/toast-store';
|
||||
import { marked } from 'marked';
|
||||
import SessionManager from './SessionManager.svelte';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
|
||||
let messagesContainer: HTMLDivElement;
|
||||
let messagesContainer: HTMLDivElement;
|
||||
|
||||
afterUpdate(() => {
|
||||
if (messagesContainer) {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
});
|
||||
|
||||
function renderMarkdown(content: string, isAssistant: boolean) {
|
||||
if (!isAssistant) return content;
|
||||
try {
|
||||
return marked.parse(content);
|
||||
} catch (error) {
|
||||
console.error('Error rendering markdown:', error);
|
||||
return content;
|
||||
}
|
||||
afterUpdate(() => {
|
||||
if (messagesContainer) {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
});
|
||||
|
||||
function renderMarkdown(content: string, isAssistant: boolean) {
|
||||
content = content.replace(/\\n/g, '\n');
|
||||
if (!isAssistant) return content;
|
||||
try {
|
||||
return marked.parse(content);
|
||||
} catch (error) {
|
||||
console.error('Error rendering markdown:', error);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bg-primary-800/30 rounded-lg flex flex-col h-full shadow-lg">
|
||||
@@ -47,7 +48,7 @@
|
||||
{/if}
|
||||
|
||||
<!-- Check 24-12-08 chat-interface for more updates here -->
|
||||
|
||||
|
||||
<div class="messages-container p-4 flex-1 overflow-y-auto max-h-dvh" bind:this={messagesContainer}>
|
||||
<div class="messages-content flex flex-col gap-4">
|
||||
{#each $chatState.messages as message}
|
||||
|
||||
153
web/src/lib/components/chat/ChatMessages.svelte.bak
Normal file
153
web/src/lib/components/chat/ChatMessages.svelte.bak
Normal file
@@ -0,0 +1,153 @@
|
||||
<script lang="ts">
|
||||
import { chatState, errorStore, streamingStore } from '$lib/store/chat-store';
|
||||
import { afterUpdate } from 'svelte';
|
||||
import { toastStore } from '$lib/store/toast-store';
|
||||
import { marked } from 'marked';
|
||||
import SessionManager from './SessionManager.svelte';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
|
||||
let messagesContainer: HTMLDivElement;
|
||||
|
||||
afterUpdate(() => {
|
||||
if (messagesContainer) {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
});
|
||||
|
||||
function renderMarkdown(content: string, isAssistant: boolean) {
|
||||
content = content.replace(/\\n/g, '\n');
|
||||
if (!isAssistant) return content;
|
||||
try {
|
||||
return marked.parse(content);
|
||||
} catch (error) {
|
||||
console.error('Error rendering markdown:', error);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bg-primary-800/30 rounded-lg flex flex-col h-full shadow-lg">
|
||||
<div class="flex justify-between items-center mb-1 mt-1 flex-none">
|
||||
<div class="flex items-center gap-2 pl-4">
|
||||
<b class="text-sm text-muted-foreground font-bold">Chat History</b>
|
||||
</div>
|
||||
<SessionManager />
|
||||
</div>
|
||||
|
||||
{#if $errorStore}
|
||||
<div class="error-message" transition:slide>
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4" role="alert">
|
||||
<p>{$errorStore}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Check 24-12-08 chat-interface for more updates here -->
|
||||
|
||||
<div class="messages-container p-4 flex-1 overflow-y-auto max-h-dvh" bind:this={messagesContainer}>
|
||||
<div class="messages-content flex flex-col gap-4">
|
||||
{#each $chatState.messages as message}
|
||||
<div
|
||||
class="message-item {message.role === 'assistant' ? 'pl-4 bg-primary/5 rounded-lg p-2' : 'pr-4 ml-auto'}"
|
||||
transition:fade
|
||||
>
|
||||
<div class="message-header flex items-center gap-2 mb-1 {message.role === 'assistant' ? '' : 'justify-end'}">
|
||||
<span class="text-xs text-muted-foreground rounded-lg p-1 variant-glass-secondary font-bold uppercase">
|
||||
{message.role === 'assistant' ? 'AI' : 'You'}
|
||||
</span>
|
||||
{#if message.role === 'assistant' && $streamingStore}
|
||||
<span class="loading-indicator flex gap-1">
|
||||
<span class="dot animate-bounce">.</span>
|
||||
<span class="dot animate-bounce delay-100">.</span>
|
||||
<span class="dot animate-bounce delay-200">.</span>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if message.role === 'assistant'}
|
||||
<div class="prose prose-slate dark:prose-invert text-inherit prose-headings:text-inherit prose-pre:bg-primary/10 prose-pre:text-inherit text-sm max-w-none">
|
||||
{@html renderMarkdown(message.content, true)}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="whitespace-pre-wrap text-sm">
|
||||
{message.content}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/*.chat-messages-wrapper {*/
|
||||
/* display: flex;*/
|
||||
/* flex-direction: column;*/
|
||||
/* min-height: 0;*/
|
||||
/*}*/
|
||||
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
-ms-overflow-style: thin;
|
||||
}
|
||||
|
||||
.messages-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
animation: blink 1.4s infinite;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
:global(.prose pre) {
|
||||
background-color: rgb(40, 44, 52);
|
||||
color: rgb(171, 178, 191);
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
:global(.prose code) {
|
||||
color: rgb(171, 178, 191);
|
||||
background-color: rgba(40, 44, 52, 0.1);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -89,6 +89,4 @@
|
||||
<NoteDrawer />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Select } from "$lib/components/ui/select";
|
||||
import { modelConfig, availableModels, loadAvailableModels } from "$lib/store/model-store";
|
||||
import { onMount } from 'svelte';
|
||||
import { Select } from "$lib/components/ui/select";
|
||||
import { modelConfig, availableModels, loadAvailableModels } from "$lib/store/model-store";
|
||||
|
||||
onMount(async () => {
|
||||
await loadAvailableModels();
|
||||
});
|
||||
onMount(async () => {
|
||||
await loadAvailableModels();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="min-w-0">
|
||||
<Select
|
||||
bind:value={$modelConfig.model}
|
||||
>
|
||||
<Select
|
||||
bind:value={$modelConfig.model}
|
||||
>
|
||||
<option value="">Default Model</option>
|
||||
{#each $availableModels as model (model.name)}
|
||||
<option value={model.name}>{model.vendor} - {model.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{#each $availableModels as model (model.name)}
|
||||
<option value={model.name}>{model.vendor} - {model.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
</script>
|
||||
|
||||
<div class="min-w-0">
|
||||
<Select
|
||||
bind:value={selectedPreset}
|
||||
>
|
||||
<Select
|
||||
bind:value={selectedPreset}
|
||||
>
|
||||
<option value="">Load a pattern...</option>
|
||||
{#each $patterns as pattern}
|
||||
<option value={pattern.Name}>{pattern.Description}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{#each $patterns as pattern}
|
||||
<option value={pattern.Name}>{pattern.Description}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -1,136 +1,136 @@
|
||||
<script lang='ts'>
|
||||
import { getToastStore } from '@skeletonlabs/skeleton';
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import { Toast } from '@skeletonlabs/skeleton';
|
||||
import { getToastStore } from '@skeletonlabs/skeleton';
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import { Toast } from '@skeletonlabs/skeleton';
|
||||
|
||||
let url = '';
|
||||
let transcript = '';
|
||||
let loading = false;
|
||||
let error = '';
|
||||
let title = '';
|
||||
let url = '';
|
||||
let transcript = '';
|
||||
let loading = false;
|
||||
let error = '';
|
||||
let title = '';
|
||||
|
||||
const toastStore = getToastStore();
|
||||
const toastStore = getToastStore();
|
||||
|
||||
async function fetchTranscript() {
|
||||
function isValidYouTubeUrl(url: string) {
|
||||
const pattern = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/;
|
||||
return pattern.test(url);
|
||||
}
|
||||
|
||||
if (!isValidYouTubeUrl(url)) {
|
||||
error = 'Please enter a valid YouTube URL';
|
||||
toastStore.trigger({
|
||||
message: error,
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ url })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to fetch transcript');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Parsed response data:', data);
|
||||
|
||||
transcript = data.transcript;
|
||||
title = data.title;
|
||||
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
async function fetchTranscript() {
|
||||
function isValidYouTubeUrl(url: string) {
|
||||
const pattern = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/;
|
||||
return pattern.test(url);
|
||||
}
|
||||
|
||||
async function copyToClipboard() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(transcript);
|
||||
toastStore.trigger({
|
||||
message: 'Transcript copied to clipboard!',
|
||||
background: 'variant-filled-success'
|
||||
});
|
||||
} catch (err) {
|
||||
toastStore.trigger({
|
||||
message: 'Failed to copy transcript',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
}
|
||||
if (!isValidYouTubeUrl(url)) {
|
||||
error = 'Please enter a valid YouTube URL';
|
||||
toastStore.trigger({
|
||||
message: error,
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ url })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to fetch transcript');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Parsed response data:', data);
|
||||
|
||||
transcript = data.transcript;
|
||||
title = data.title;
|
||||
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyToClipboard() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(transcript);
|
||||
toastStore.trigger({
|
||||
message: 'Transcript copied to clipboard!',
|
||||
background: 'variant-filled-success'
|
||||
});
|
||||
} catch (err) {
|
||||
toastStore.trigger({
|
||||
message: 'Failed to copy transcript',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={url}
|
||||
placeholder="YouTube URL"
|
||||
class="flex-1 rounded-full border bg-background px-4"
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
on:click={fetchTranscript}
|
||||
disabled={loading || !url}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={url}
|
||||
placeholder="YouTube URL"
|
||||
class="flex-1 rounded-full border bg-background px-4"
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
on:click={fetchTranscript}
|
||||
disabled={loading || !url}
|
||||
>
|
||||
{#if loading}
|
||||
<div class="spinner-border" />
|
||||
<div class="spinner-border" />
|
||||
{:else}
|
||||
Get
|
||||
Get
|
||||
{/if}
|
||||
</Button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="bg-destructive/15 text-destructive rounded-lg p-2">{error}</div>
|
||||
<div class="bg-destructive/15 text-destructive rounded-lg p-2">{error}</div>
|
||||
{/if}
|
||||
|
||||
{#if transcript}
|
||||
<Toast position="b" />
|
||||
<div class="space-y-4 border rounded-lg p-4 bg-muted/50 h-96">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-xs font-semibold">{title || 'Transcript'}</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
on:click={copyToClipboard}
|
||||
>
|
||||
Copy to Clipboard
|
||||
</Button>
|
||||
</div>
|
||||
<textarea
|
||||
class="w-full text-xs rounded-md border bg-background px-3 py-2 resize-none h-72"
|
||||
readonly
|
||||
value={transcript}
|
||||
></textarea>
|
||||
<Toast position="b" />
|
||||
<div class="space-y-4 border rounded-lg p-4 bg-muted/50 h-96">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-xs font-semibold">{title || 'Transcript'}</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
on:click={copyToClipboard}
|
||||
>
|
||||
Copy to Clipboard
|
||||
</Button>
|
||||
</div>
|
||||
<textarea
|
||||
class="w-full text-xs rounded-md border bg-background px-3 py-2 resize-none h-72"
|
||||
readonly
|
||||
value={transcript}
|
||||
></textarea>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.spinner-border {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.75s linear infinite;
|
||||
}
|
||||
<style>
|
||||
.spinner-border {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.75s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Contact } from 'lucide-svelte';
|
||||
import { Contact } from 'lucide-svelte';
|
||||
</script>
|
||||
|
||||
<div class="form-control w-full m-auto p-4 rounded-lg bg-gradient-to-br variant-gradient-success-warning shadow-lg text-current" title="contact form">
|
||||
<h2 class="font-bold pl-2">We'd love to hear from you</h2>
|
||||
<p class="font-bold pl-2">Email</p>
|
||||
<div class="input-group input-group-divider grid-cols-[1fr_auto]">
|
||||
<input type="text" placeholder="Enter an email address where you can be reached..." />
|
||||
</div>
|
||||
<p class="font-bold pl-2">Website</p>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
|
||||
<div class="input-group-shim">https://</div>
|
||||
<input type="text" placeholder="www.example.com" />
|
||||
</div>
|
||||
<p class="font-bold pl-2">Contact Information</p>
|
||||
<div class="input-group input-group-divider grid-cols-[1fr_auto]">
|
||||
<input type="text" placeholder="Enter a other contact information here..." />
|
||||
</div>
|
||||
<label class="label">
|
||||
<span class="font-bold pl-2">Message</span>
|
||||
<textarea class="textarea" rows="4" placeholder="Enter your message ..." />
|
||||
</label>
|
||||
<a href="/" title=""><button class="button variant-filled-secondary rounded-lg p-2"><Contact /></button></a>
|
||||
|
||||
<h2 class="font-bold pl-2">We'd love to hear from you</h2>
|
||||
<p class="font-bold pl-2">Email</p>
|
||||
<div class="input-group input-group-divider grid-cols-[1fr_auto]">
|
||||
<input type="text" placeholder="Enter an email address where you can be reached..." />
|
||||
</div>
|
||||
<p class="font-bold pl-2">Website</p>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
|
||||
<div class="input-group-shim">https://</div>
|
||||
<input type="text" placeholder="www.example.com" />
|
||||
</div>
|
||||
<p class="font-bold pl-2">Contact Information</p>
|
||||
<div class="input-group input-group-divider grid-cols-[1fr_auto]">
|
||||
<input type="text" placeholder="Enter a other contact information here..." />
|
||||
</div>
|
||||
<label class="label">
|
||||
<span class="font-bold pl-2">Message</span>
|
||||
<textarea class="textarea" rows="4" placeholder="Enter your message ..." />
|
||||
</label>
|
||||
<a href="/" title=""><button class="button variant-filled-secondary rounded-lg p-2"><Contact /></button></a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { formatDistance } from 'date-fns';
|
||||
import type { Post } from '$lib/interfaces/post-interface';
|
||||
import PostMeta from './PostMeta.svelte';
|
||||
import Card from '$lib/components/ui/cards/card.svelte';
|
||||
import { cn } from '$lib/utils/utils';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import type { Post } from './post-interface';
|
||||
import PostMeta from './PostMeta.svelte';
|
||||
import Card from '$lib/components/ui/cards/card.svelte';
|
||||
import { cn } from '$lib/utils/utils';
|
||||
|
||||
export let post: Post;
|
||||
export let className: string = '';
|
||||
export let post: Post;
|
||||
export let className: string = '';
|
||||
|
||||
function parseDate(dateStr: string): Date {
|
||||
// Handle both ISO strings and YYYY-MM-DD formats
|
||||
return new Date(dateStr);
|
||||
}
|
||||
function parseDate(dateStr: string): Date {
|
||||
// Handle both ISO strings and YYYY-MM-DD formats
|
||||
return new Date(dateStr);
|
||||
}
|
||||
</script>
|
||||
|
||||
<article class="card card-hover group relative rounded-lg border p-6 hover:bg-primary-500/50 {className}">
|
||||
<a
|
||||
href="/posts/{post.slug}"
|
||||
class="absolute inset-0"
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
<span class="sr-only">View {post.metadata?.title}</span>
|
||||
</a>
|
||||
<div class="flex flex-col justify-between space-y-4">
|
||||
<div class="space-y-2">
|
||||
<img src={post.metadata?.images?.[0]} alt="" class="rounded-lg" />
|
||||
<h2 class="text-xl font-semibold tracking-tight">{post.metadata?.title}</h2>
|
||||
<p class="text-muted-foreground">{post.metadata?.description}</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
|
||||
<time datetime={post.metadata?.date}>
|
||||
{#if post.metadata?.date}
|
||||
{formatDistance(parseDate(post.metadata.date), new Date(), { addSuffix: false })}
|
||||
{/if}
|
||||
</time>
|
||||
{#if post.metadata?.tags?.length > 0}
|
||||
<span class="text-xs">•</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each post.metadata?.tags as tag}
|
||||
<a
|
||||
href="/tags/{tag}"
|
||||
class="inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors hover:bg-secondary"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<a
|
||||
href="/posts/{post.slug}"
|
||||
class="absolute inset-0"
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
<span class="sr-only">View {post.metadata?.title}</span>
|
||||
</a>
|
||||
<div class="flex flex-col justify-between space-y-4">
|
||||
<div class="space-y-2">
|
||||
<img src={post.metadata?.images?.[0]} alt="" class="rounded-lg" />
|
||||
<h2 class="text-xl font-semibold tracking-tight">{post.metadata?.title}</h2>
|
||||
<p class="text-muted-foreground">{post.metadata?.description}</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
|
||||
<time datetime={post.metadata?.date}>
|
||||
{#if post.metadata?.date}
|
||||
{formatDistance(parseDate(post.metadata.date), new Date(), { addSuffix: false })}
|
||||
{/if}
|
||||
</time>
|
||||
{#if post.metadata?.tags?.length > 0}
|
||||
<span class="text-xs">•</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each post.metadata?.tags as tag}
|
||||
<a
|
||||
href="/tags/{tag}"
|
||||
class="inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors hover:bg-secondary"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
<script lang="ts">
|
||||
import PostMeta from './PostMeta.svelte';
|
||||
import type { Post } from '$lib/interfaces/post-interface'
|
||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||
import SideNav from '$lib/components/ui/side-nav/SideNav.svelte';
|
||||
import PostMeta from './PostMeta.svelte';
|
||||
import type { Post } from './post-interface'
|
||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||
import SideNav from '$lib/components/ui/side-nav/SideNav.svelte';
|
||||
|
||||
export let post: Post;
|
||||
export let post: Post;
|
||||
</script>
|
||||
|
||||
<article class="py-6">
|
||||
{#if !post?.content || !post?.metadata}
|
||||
<div class="flex min-h-[400px] items-center justify-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Spinner class="h-6 w-6" />
|
||||
<span class="text-sm text-muted-foreground">Loading post...</span>
|
||||
</div>
|
||||
{#if !post?.content || !post?.metadata}
|
||||
<div class="flex min-h-[400px] items-center justify-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Spinner class="h-6 w-6" />
|
||||
<span class="text-sm text-muted-foreground">Loading post...</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4 pl-8 ml-8">
|
||||
<h1 class="inline-block text-4xl font-bold inherit-colors lg:text-5xl">{post.metadata.title}</h1>
|
||||
<PostMeta data={post.metadata} />
|
||||
</div>
|
||||
<div class="items-center py-8 mx-auto gap-8 max-w-7xl relative prose prose-slate dark:prose-invert">
|
||||
{#if typeof post.content === 'function'}
|
||||
<SideNav />
|
||||
<svelte:component this={post.content} />
|
||||
{:else if typeof post.content === 'string'}
|
||||
{post.content}
|
||||
{:else}
|
||||
<div class="flex gap-2">
|
||||
<Spinner class="h-8 w-8" />
|
||||
<span class="text-sm text-muted-foreground">Loading content...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4 pl-8 ml-8">
|
||||
<h1 class="inline-block text-4xl font-bold inherit-colors lg:text-5xl">{post.metadata.title}</h1>
|
||||
<PostMeta data={post.metadata} />
|
||||
</div>
|
||||
<div class="items-center py-8 mx-auto gap-8 max-w-7xl relative prose prose-slate dark:prose-invert">
|
||||
{#if typeof post.content === 'function'}
|
||||
<SideNav />
|
||||
<svelte:component this={post.content} />
|
||||
{:else if typeof post.content === 'string'}
|
||||
{post.content}
|
||||
{:else}
|
||||
<div class="flex gap-2">
|
||||
<Spinner class="h-8 w-8" />
|
||||
<span class="text-sm text-muted-foreground">Loading content...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
<script>
|
||||
export let aliases
|
||||
export let date
|
||||
export let tags
|
||||
export let title
|
||||
export let description
|
||||
export let author
|
||||
export let updated
|
||||
//export let content
|
||||
export let aliases
|
||||
export let date
|
||||
export let tags
|
||||
export let title
|
||||
export let description
|
||||
export let author
|
||||
export let updated
|
||||
//export let content
|
||||
</script>
|
||||
|
||||
<article class="prose prose-slate dark:prose-invert max-w-5xl flex-1">
|
||||
<h1 class="inline-block text-4xl font-bold inherit-colors lg:text-5xl">{aliases}</h1>
|
||||
<slot />
|
||||
<h1 class="inline-block text-4xl font-bold inherit-colors lg:text-5xl">{aliases}</h1>
|
||||
<slot />
|
||||
</article>
|
||||
|
||||
<style lang="postcss">
|
||||
:global(textarea) {
|
||||
@apply textarea;
|
||||
}
|
||||
:global(textarea) {
|
||||
@apply textarea;
|
||||
}
|
||||
|
||||
:global(h1) {
|
||||
@apply h1;
|
||||
}
|
||||
:global(h1) {
|
||||
@apply h1;
|
||||
}
|
||||
|
||||
:global(h2) {
|
||||
@apply h2;
|
||||
}
|
||||
:global(h2) {
|
||||
@apply h2;
|
||||
}
|
||||
|
||||
:global(p) {
|
||||
@layer p;
|
||||
}
|
||||
:global(p) {
|
||||
@layer p;
|
||||
}
|
||||
|
||||
:global(ul) {
|
||||
@layer ul;
|
||||
}
|
||||
:global(ul) {
|
||||
@layer ul;
|
||||
}
|
||||
|
||||
:global(li) {
|
||||
@layer li;
|
||||
}
|
||||
:global(li) {
|
||||
@layer li;
|
||||
}
|
||||
|
||||
:global(a) {
|
||||
@layer a;
|
||||
}
|
||||
:global(a) {
|
||||
@layer a;
|
||||
}
|
||||
|
||||
:global(img) {
|
||||
@layer img;
|
||||
}
|
||||
:global(img) {
|
||||
@layer img;
|
||||
}
|
||||
|
||||
:global(blockquote) {
|
||||
@layer blockquote;
|
||||
}
|
||||
:global(blockquote) {
|
||||
@layer blockquote;
|
||||
}
|
||||
|
||||
:global(code) {
|
||||
@layer code;
|
||||
}
|
||||
:global(code) {
|
||||
@layer code;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { formatDistance } from 'date-fns';
|
||||
import type { PostMetadata } from '$lib/interfaces/post-interface';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import type { PostMetadata } from './post-interface';
|
||||
|
||||
export let data: PostMetadata;
|
||||
export let showUpdated = true;
|
||||
export let data: PostMetadata;
|
||||
export let showUpdated = true;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 text-sm text-muted-foreground">
|
||||
<div class="flex items-center gap-2">
|
||||
<span><b>Published on: </b>{data.date}</span>
|
||||
{#if showUpdated && data.updated !== data.date}
|
||||
<span>· Updated on {data.updated}</span>
|
||||
{/if}
|
||||
<div class="flex items-center gap-2">
|
||||
<span><b>Published on: </b>{data.date}</span>
|
||||
{#if showUpdated && data.updated !== data.date}
|
||||
<span>· Updated on {data.updated}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if data.author}
|
||||
<div class="text-xs font-bold">By {data.author}</div>
|
||||
{/if}
|
||||
{#if data.description}
|
||||
<p class="text-base font-bold">{data.description}</p>
|
||||
{/if}
|
||||
{#if data.tags && data.tags.length > 0}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each data.tags as tag}
|
||||
<a
|
||||
href="/tags/{tag}"
|
||||
class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors hover:bg-muted/50"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if data.author}
|
||||
<div class="text-xs font-bold">By {data.author}</div>
|
||||
{/if}
|
||||
{#if data.description}
|
||||
<p class="text-base font-bold">{data.description}</p>
|
||||
{/if}
|
||||
{#if data.tags && data.tags.length > 0}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each data.tags as tag}
|
||||
<a
|
||||
href="/tags/{tag}"
|
||||
class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors hover:bg-muted/50"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Extract Wisdom
|
||||
date: 2024-01-01
|
||||
description: Something here
|
||||
description: Pattern - Extract Wisdom
|
||||
updated:
|
||||
aliases: Extract Wisdom
|
||||
tags:
|
||||
|
||||
@@ -3,15 +3,15 @@ import { toastStore } from '$lib/store/toast-store';
|
||||
const toastStoreInstance = toastStore;
|
||||
|
||||
export const toastService = {
|
||||
success(message: string) {
|
||||
toastStoreInstance.success(message);
|
||||
},
|
||||
success(message: string) {
|
||||
toastStoreInstance.success(message);
|
||||
},
|
||||
|
||||
error(message: string) {
|
||||
toastStoreInstance.error(message);
|
||||
},
|
||||
error(message: string) {
|
||||
toastStoreInstance.error(message);
|
||||
},
|
||||
|
||||
info(message: string) {
|
||||
toastStoreInstance.info(message);
|
||||
}
|
||||
info(message: string) {
|
||||
toastStoreInstance.info(message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { YoutubeTranscript } from 'youtube-transcript';
|
||||
|
||||
export interface TranscriptResponse {
|
||||
transcript: string;
|
||||
title: string;
|
||||
transcript: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export async function getTranscript(url: string): Promise<TranscriptResponse> {
|
||||
try {
|
||||
const videoId = extractVideoId(url);
|
||||
if (!videoId) {
|
||||
throw new Error('Invalid YouTube URL');
|
||||
}
|
||||
|
||||
const transcriptItems = await YoutubeTranscript.fetchTranscript(videoId);
|
||||
const transcript = transcriptItems
|
||||
.map(item => item.text)
|
||||
.join(' ');
|
||||
|
||||
const transcriptTitle = transcriptItems
|
||||
.map(item => item.text)
|
||||
.join('');
|
||||
|
||||
// TODO: Add title fetching
|
||||
return {
|
||||
transcript,
|
||||
title: videoId // Just returning the video ID as title
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Transcript fetch error:', error);
|
||||
throw new Error('Failed to fetch transcript');
|
||||
try {
|
||||
const videoId = extractVideoId(url);
|
||||
if (!videoId) {
|
||||
throw new Error('Invalid YouTube URL');
|
||||
}
|
||||
|
||||
const transcriptItems = await YoutubeTranscript.fetchTranscript(videoId);
|
||||
const transcript = transcriptItems
|
||||
.map(item => item.text)
|
||||
.join(' ');
|
||||
|
||||
const transcriptTitle = transcriptItems
|
||||
.map(item => item.text)
|
||||
.join('');
|
||||
|
||||
// TODO: Add title fetching
|
||||
return {
|
||||
transcript,
|
||||
title: videoId // Just returning the video ID as title
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Transcript fetch error:', error);
|
||||
throw new Error('Failed to fetch transcript');
|
||||
}
|
||||
}
|
||||
|
||||
function extractVideoId(url: string): string | null {
|
||||
const match = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
const match = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
@@ -2,23 +2,23 @@ import { writable } from 'svelte/store';
|
||||
import type { ChatConfig } from '$lib/types/interfaces/chat-interface';
|
||||
|
||||
const defaultConfig: ChatConfig = {
|
||||
temperature: 0.7,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0
|
||||
temperature: 0.7,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0
|
||||
};
|
||||
|
||||
export const chatConfig = writable<ChatConfig>(defaultConfig);
|
||||
|
||||
export function updateConfig(newConfig: Partial<ChatConfig>): void {
|
||||
chatConfig.update(config => ({
|
||||
...config,
|
||||
...newConfig
|
||||
}));
|
||||
chatConfig.update(config => ({
|
||||
...config,
|
||||
...newConfig
|
||||
}));
|
||||
}
|
||||
|
||||
export function resetConfig(): void {
|
||||
chatConfig.set(defaultConfig);
|
||||
chatConfig.set(defaultConfig);
|
||||
}
|
||||
|
||||
export { type ChatConfig };
|
||||
|
||||
@@ -6,63 +6,63 @@ export const patterns = writable<Pattern[]>([]);
|
||||
export const systemPrompt = writable<string>('');
|
||||
|
||||
export const setSystemPrompt = (prompt: string) => {
|
||||
console.log('Setting system prompt:', prompt);
|
||||
systemPrompt.set(prompt);
|
||||
console.log('Current system prompt:', get(systemPrompt));
|
||||
console.log('Setting system prompt:', prompt);
|
||||
systemPrompt.set(prompt);
|
||||
console.log('Current system prompt:', get(systemPrompt));
|
||||
};
|
||||
|
||||
export const patternAPI = {
|
||||
...createStorageAPI<Pattern>('patterns'),
|
||||
...createStorageAPI<Pattern>('patterns'),
|
||||
|
||||
async loadPatterns() {
|
||||
async loadPatterns() {
|
||||
try {
|
||||
const response = await fetch(`/api/patterns/names`);
|
||||
const data = await response.json();
|
||||
console.log("Load Patterns:", data);
|
||||
|
||||
// Create an array of promises to fetch all pattern contents
|
||||
const patternsPromises = data.map(async (pattern: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/patterns/names`);
|
||||
const data = await response.json();
|
||||
console.log("Load Patterns:", data);
|
||||
|
||||
// Create an array of promises to fetch all pattern contents
|
||||
const patternsPromises = data.map(async (pattern: string) => {
|
||||
try {
|
||||
const patternResponse = await fetch(`/api/patterns/${pattern}`);
|
||||
const patternData = await patternResponse.json();
|
||||
return {
|
||||
Name: pattern,
|
||||
Description: pattern.charAt(0).toUpperCase() + pattern.slice(1),
|
||||
Pattern: patternData.Pattern
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to load pattern ${pattern}:`, error);
|
||||
return {
|
||||
Name: pattern,
|
||||
Description: pattern.charAt(0).toUpperCase() + pattern.slice(1),
|
||||
Pattern: ""
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for all pattern contents to be fetched
|
||||
const loadedPatterns = await Promise.all(patternsPromises);
|
||||
console.log("Patterns with content:", loadedPatterns);
|
||||
patterns.set(loadedPatterns);
|
||||
return loadedPatterns;
|
||||
const patternResponse = await fetch(`/api/patterns/${pattern}`);
|
||||
const patternData = await patternResponse.json();
|
||||
return {
|
||||
Name: pattern,
|
||||
Description: pattern.charAt(0).toUpperCase() + pattern.slice(1),
|
||||
Pattern: patternData.Pattern
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to load patterns:', error);
|
||||
patterns.set([]);
|
||||
return [];
|
||||
console.error(`Failed to load pattern ${pattern}:`, error);
|
||||
return {
|
||||
Name: pattern,
|
||||
Description: pattern.charAt(0).toUpperCase() + pattern.slice(1),
|
||||
Pattern: ""
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
selectPattern(patternName: string) {
|
||||
const allPatterns = get(patterns);
|
||||
console.log('Selecting pattern:', patternName);
|
||||
const selectedPattern = allPatterns.find(p => p.Name === patternName);
|
||||
if (selectedPattern) {
|
||||
console.log('Found pattern content:', selectedPattern.Pattern);
|
||||
setSystemPrompt(selectedPattern.Pattern.trim());
|
||||
} else {
|
||||
console.log('No pattern found for name:', patternName);
|
||||
setSystemPrompt('');
|
||||
}
|
||||
console.log('System prompt store value after setting:', get(systemPrompt));
|
||||
// Wait for all pattern contents to be fetched
|
||||
const loadedPatterns = await Promise.all(patternsPromises);
|
||||
console.log("Patterns with content:", loadedPatterns);
|
||||
patterns.set(loadedPatterns);
|
||||
return loadedPatterns;
|
||||
} catch (error) {
|
||||
console.error('Failed to load patterns:', error);
|
||||
patterns.set([]);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
selectPattern(patternName: string) {
|
||||
const allPatterns = get(patterns);
|
||||
console.log('Selecting pattern:', patternName);
|
||||
const selectedPattern = allPatterns.find(p => p.Name === patternName);
|
||||
if (selectedPattern) {
|
||||
console.log('Found pattern content:', selectedPattern.Pattern);
|
||||
setSystemPrompt(selectedPattern.Pattern.trim());
|
||||
} else {
|
||||
console.log('No pattern found for name:', patternName);
|
||||
setSystemPrompt('');
|
||||
}
|
||||
console.log('System prompt store value after setting:', get(systemPrompt));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,79 +8,79 @@ import { toastService } from '../services/toast-service';
|
||||
export const sessions = writable<Session[]>([]);
|
||||
|
||||
export const sessionAPI = {
|
||||
...createStorageAPI<Session>('sessions'),
|
||||
...createStorageAPI<Session>('sessions'),
|
||||
|
||||
async loadSessions() {
|
||||
async loadSessions() {
|
||||
try {
|
||||
const response = await fetch(`/api/sessions/names`);
|
||||
const sessionNames: string[] = await response.json();
|
||||
|
||||
const sessionPromises = sessionNames.map(async (name: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/sessions/names`);
|
||||
const sessionNames: string[] = await response.json();
|
||||
|
||||
const sessionPromises = sessionNames.map(async (name: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/sessions/${name}`);
|
||||
const data = await response.json();
|
||||
return {
|
||||
Name: name,
|
||||
Message: Array.isArray(data.Message) ? data.Message : [],
|
||||
Session: data.Session
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error loading session ${name}:`, error);
|
||||
return {
|
||||
Name: name,
|
||||
Message: [],
|
||||
Session: ""
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const sessionsData = await Promise.all(sessionPromises);
|
||||
sessions.set(sessionsData);
|
||||
return sessionsData;
|
||||
const response = await fetch(`/api/sessions/${name}`);
|
||||
const data = await response.json();
|
||||
return {
|
||||
Name: name,
|
||||
Message: Array.isArray(data.Message) ? data.Message : [],
|
||||
Session: data.Session
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading sessions:', error);
|
||||
sessions.set([]);
|
||||
return [];
|
||||
console.error(`Error loading session ${name}:`, error);
|
||||
return {
|
||||
Name: name,
|
||||
Message: [],
|
||||
Session: ""
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
selectSession(sessionName: string) {
|
||||
const allSessions = get(sessions);
|
||||
const selectedSession = allSessions.find(session => session.Name === sessionName);
|
||||
if (selectedSession) {
|
||||
sessions.set([selectedSession]);
|
||||
} else {
|
||||
sessions.set([]);
|
||||
}
|
||||
},
|
||||
|
||||
async exportToFile(messages: Message[]) {
|
||||
try {
|
||||
await saveToFile(messages, 'session-history.json');
|
||||
toastService.success('Session exported successfully');
|
||||
} catch (error) {
|
||||
toastService.error('Failed to export session');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async importFromFile(): Promise<Message[]> {
|
||||
try {
|
||||
const file = await openFileDialog('.json');
|
||||
if (!file) {
|
||||
throw new Error('No file selected');
|
||||
}
|
||||
|
||||
const content = await readFileAsJson<Message[]>(file);
|
||||
if (!Array.isArray(content)) {
|
||||
throw new Error('Invalid session file format');
|
||||
}
|
||||
|
||||
toastService.success('Session imported successfully');
|
||||
return content;
|
||||
} catch (error) {
|
||||
toastService.error(error instanceof Error ? error.message : 'Failed to import session');
|
||||
throw error;
|
||||
}
|
||||
const sessionsData = await Promise.all(sessionPromises);
|
||||
sessions.set(sessionsData);
|
||||
return sessionsData;
|
||||
} catch (error) {
|
||||
console.error('Error loading sessions:', error);
|
||||
sessions.set([]);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
selectSession(sessionName: string) {
|
||||
const allSessions = get(sessions);
|
||||
const selectedSession = allSessions.find(session => session.Name === sessionName);
|
||||
if (selectedSession) {
|
||||
sessions.set([selectedSession]);
|
||||
} else {
|
||||
sessions.set([]);
|
||||
}
|
||||
},
|
||||
|
||||
async exportToFile(messages: Message[]) {
|
||||
try {
|
||||
await saveToFile(messages, 'session-history.json');
|
||||
toastService.success('Session exported successfully');
|
||||
} catch (error) {
|
||||
toastService.error('Failed to export session');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async importFromFile(): Promise<Message[]> {
|
||||
try {
|
||||
const file = await openFileDialog('.json');
|
||||
if (!file) {
|
||||
throw new Error('No file selected');
|
||||
}
|
||||
|
||||
const content = await readFileAsJson<Message[]>(file);
|
||||
if (!Array.isArray(content)) {
|
||||
throw new Error('Invalid session file format');
|
||||
}
|
||||
|
||||
toastService.success('Session imported successfully');
|
||||
return content;
|
||||
} catch (error) {
|
||||
toastService.error(error instanceof Error ? error.message : 'Failed to import session');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export interface ToastMessage {
|
||||
message: string;
|
||||
type: 'success' | 'error' | 'info';
|
||||
id: number;
|
||||
message: string;
|
||||
type: 'success' | 'error' | 'info';
|
||||
id: number;
|
||||
}
|
||||
|
||||
function createToastStore() {
|
||||
const { subscribe, update } = writable<ToastMessage[]>([]);
|
||||
let nextId = 1;
|
||||
const { subscribe, update } = writable<ToastMessage[]>([]);
|
||||
let nextId = 1;
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
success: (message: string) => {
|
||||
update(toasts => [...toasts, { message, type: 'success', id: nextId++ }]);
|
||||
},
|
||||
error: (message: string) => {
|
||||
update(toasts => [...toasts, { message, type: 'error', id: nextId++ }]);
|
||||
},
|
||||
info: (message: string) => {
|
||||
update(toasts => [...toasts, { message, type: 'info', id: nextId++ }]);
|
||||
},
|
||||
remove: (id: number) => {
|
||||
update(toasts => toasts.filter(t => t.id !== id));
|
||||
}
|
||||
};
|
||||
return {
|
||||
subscribe,
|
||||
success: (message: string) => {
|
||||
update(toasts => [...toasts, { message, type: 'success', id: nextId++ }]);
|
||||
},
|
||||
error: (message: string) => {
|
||||
update(toasts => [...toasts, { message, type: 'error', id: nextId++ }]);
|
||||
},
|
||||
info: (message: string) => {
|
||||
update(toasts => [...toasts, { message, type: 'info', id: nextId++ }]);
|
||||
},
|
||||
remove: (id: number) => {
|
||||
update(toasts => toasts.filter(t => t.id !== id));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const toastStore = createToastStore();
|
||||
|
||||
@@ -3,46 +3,46 @@ export type ResponseFormat = 'markdown' | 'mermaid' | 'plain';
|
||||
export type ResponseType = 'content' | 'error' | 'complete';
|
||||
|
||||
export interface ChatPrompt {
|
||||
userInput: string;
|
||||
systemPrompt: string;
|
||||
model: string;
|
||||
patternName: string;
|
||||
userInput: string;
|
||||
systemPrompt: string;
|
||||
model: string;
|
||||
patternName: string;
|
||||
}
|
||||
|
||||
export interface ChatConfig {
|
||||
temperature: number;
|
||||
top_p: number;
|
||||
frequency_penalty: number;
|
||||
presence_penalty: number;
|
||||
temperature: number;
|
||||
top_p: number;
|
||||
frequency_penalty: number;
|
||||
presence_penalty: number;
|
||||
}
|
||||
|
||||
export interface ChatRequest {
|
||||
prompts: ChatPrompt[];
|
||||
messages: Message[];
|
||||
temperature: number;
|
||||
top_p: number;
|
||||
frequency_penalty: number;
|
||||
presence_penalty: number;
|
||||
prompts: ChatPrompt[];
|
||||
messages: Message[];
|
||||
temperature: number;
|
||||
top_p: number;
|
||||
frequency_penalty: number;
|
||||
presence_penalty: number;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
role: MessageRole;
|
||||
content: string;
|
||||
role: MessageRole;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ChatState {
|
||||
messages: Message[];
|
||||
isStreaming: boolean;
|
||||
messages: Message[];
|
||||
isStreaming: boolean;
|
||||
}
|
||||
|
||||
export interface StreamResponse {
|
||||
type: ResponseType;
|
||||
format: ResponseFormat;
|
||||
content: string;
|
||||
type: ResponseType;
|
||||
format: ResponseFormat;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ChatError {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
}
|
||||
code: string;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface Context {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export interface VendorModel {
|
||||
name: string;
|
||||
vendor: string;
|
||||
name: string;
|
||||
vendor: string;
|
||||
}
|
||||
|
||||
|
||||
export interface ModelsResponse {
|
||||
models: string[];
|
||||
vendors: Record<string, string[]>;
|
||||
@@ -10,10 +10,10 @@ export interface ModelsResponse {
|
||||
|
||||
|
||||
export interface ModelConfig {
|
||||
model: string;
|
||||
temperature: number;
|
||||
top_p: number;
|
||||
maxLength: number;
|
||||
frequency: number;
|
||||
presence: number;
|
||||
model: string;
|
||||
temperature: number;
|
||||
top_p: number;
|
||||
maxLength: number;
|
||||
frequency: number;
|
||||
presence: number;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface Pattern {
|
||||
Name: string;
|
||||
Description: string;
|
||||
Pattern: string; // | object
|
||||
}
|
||||
Name: string;
|
||||
Description: string;
|
||||
Pattern: string; // | object
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Message } from "./chat-interface";
|
||||
|
||||
export interface Session {
|
||||
Name: string;
|
||||
Message: string | Message[];
|
||||
Session: string | object;
|
||||
Name: string;
|
||||
Message: string | Message[];
|
||||
Session: string | object;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface StorageEntity {
|
||||
Name: string;
|
||||
Description?: string;
|
||||
Pattern?: string | object;
|
||||
Session?: string | object;
|
||||
Context?: string | object;
|
||||
}
|
||||
Name: string;
|
||||
Description?: string;
|
||||
Pattern?: string | object;
|
||||
Session?: string | object;
|
||||
Context?: string | object;
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
export async function openFileDialog(accept: string): Promise<File | null> {
|
||||
return new Promise((resolve) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = accept;
|
||||
|
||||
input.onchange = (event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
resolve(file || null);
|
||||
};
|
||||
|
||||
input.click();
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = accept;
|
||||
|
||||
input.onchange = (event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
resolve(file || null);
|
||||
};
|
||||
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
export async function readFileAsJson<T>(file: File): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const data = JSON.parse(reader.result as string);
|
||||
resolve(data);
|
||||
} catch (error) {
|
||||
reject(new Error('Invalid JSON format in file'));
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const data = JSON.parse(reader.result as string);
|
||||
resolve(data);
|
||||
} catch (error) {
|
||||
reject(new Error('Invalid JSON format in file'));
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveToFile(data: any, filename: string): Promise<void> {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function validateYouTubeUrl(url:string) {
|
||||
const pattern = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/;
|
||||
return pattern.test(url);
|
||||
}
|
||||
const pattern = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/;
|
||||
return pattern.test(url);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script>
|
||||
import SideNav from '$lib/components/ui/side-nav/SideNav.svelte';
|
||||
import Content from './README.md';
|
||||
import SideNav from '$lib/components/ui/side-nav/SideNav.svelte';
|
||||
import Content from './README.md';
|
||||
</script>
|
||||
|
||||
{#if Content}
|
||||
<div class="items-center mx-auto pt-8 grid-cols-[80%_20%] grid gap-8 max-w-7xl relative">
|
||||
<svelte:component this={Content} />
|
||||
<SideNav />
|
||||
</div>
|
||||
<div class="items-center mx-auto pt-8 grid-cols-[80%_20%] grid gap-8 max-w-7xl relative">
|
||||
<svelte:component this={Content} />
|
||||
<SideNav />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="container py-12">
|
||||
<div class="container py-12">
|
||||
<h1 class="mb-8 text-3xl font-bold">Sorry</h1>
|
||||
<div class="flex min-h-[400px] items-center justify-center text-center">
|
||||
<p class="text-lg font-medium">Nothing found</p>
|
||||
<p class="mt-2 text-sm text-muted-foreground">Check back later for new content.</p>
|
||||
<p class="text-lg font-medium">Nothing found</p>
|
||||
<p class="mt-2 text-sm text-muted-foreground">Check back later for new content.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -3,19 +3,6 @@ import type { PageLoad } from './$types';
|
||||
import type { Frontmatter } from '$lib/types/markdown';
|
||||
|
||||
const posts = import.meta.glob<{ metadata: Frontmatter, default: unknown }>('/src/lib/content/posts/*.{md,svx}', { eager: true });
|
||||
// Refractor to use the Frontmatter type from $lib/types/markdown
|
||||
//const posts = import.meta.glob<{
|
||||
// metadata: {
|
||||
// title: string;
|
||||
// description: string;
|
||||
// date: string;
|
||||
// tags: string[];
|
||||
// updated?: string;
|
||||
// author?: string;
|
||||
// aliases?: string[];
|
||||
// };
|
||||
// default: unknown;
|
||||
//}>('/src/lib/content/posts/*.{md,svx}', { eager: true });
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
const post = Object.entries(posts).find(([path]) =>
|
||||
|
||||
Reference in New Issue
Block a user