This commit is contained in:
John
2024-12-16 18:40:15 -05:00
parent 4c0ed0a5f0
commit 9b38c8d5aa
86 changed files with 1691 additions and 1121 deletions

5
go.mod
View File

@@ -6,14 +6,15 @@ toolchain go1.23.1
require (
github.com/anaskhan96/soup v1.2.5
github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4
github.com/atotto/clipboard v0.1.4
github.com/gabriel-vasile/mimetype v1.4.6
github.com/gin-gonic/gin v1.10.0
github.com/go-git/go-git/v5 v5.12.0
github.com/go-shiori/go-readability v0.0.0-20241012063810-92284fa8a71f
github.com/google/generative-ai-go v0.18.0
github.com/jessevdk/go-flags v1.6.1
github.com/joho/godotenv v1.5.1
github.com/liushuangls/go-anthropic/v2 v2.11.0
github.com/ollama/ollama v0.4.1
github.com/otiai10/copy v1.14.0
github.com/pkg/errors v0.9.1
@@ -35,7 +36,6 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.2 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
github.com/bytedance/sonic v1.12.4 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect
@@ -58,7 +58,6 @@ require (
github.com/goccy/go-json v0.10.3 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/generative-ai-go v0.18.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect

2
go.sum
View File

@@ -158,8 +158,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/liushuangls/go-anthropic/v2 v2.11.0 h1:YKyxDWQNaKPPgtLCgBH+JqzuznNWw8ZqQVeSdQNDMds=
github.com/liushuangls/go-anthropic/v2 v2.11.0/go.mod h1:8BKv/fkeTaL5R9R9bGkaknYBueyw2WxY20o7bImbOek=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=

View File

@@ -1,3 +1,3 @@
package main
var version = "v..1"
var version = "v1.4.120"

4
web/package-lock.json generated
View File

@@ -1,11 +1,11 @@
{
"name": "terminal-blog",
"name": "fabric",
"version": "0.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "terminal-blog",
"name": "fabric",
"version": "0.0.1",
"dependencies": {
"@floating-ui/dom": "^1.5.3",

View File

@@ -1,5 +1,5 @@
{
"name": "terminal-blog",
"name": "fabric",
"version": "0.0.1",
"private": true,
"scripts": {
@@ -24,10 +24,12 @@
"autoprefixer": "^10.4.16",
"lucide-svelte": "^0.309.0",
"mdsvex": "^0.11.0",
"postcss": "^8.4.32",
"postcss": "^8.4.49",
"postcss-load-config": "^5.0.2",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"svelte-inview": "^4.0.4",
"svelte-reveal": "^1.1.0",
"svelte-youtube-embed": "^0.3.3",
"svelte-youtube-lite": "^0.6.2",
"tailwindcss": "^3.3.6",
@@ -64,7 +66,8 @@
"hawk@<9.0.1": ">=9.0.1",
"qs@<6.2.4": ">=6.2.4",
"cookie@<0.7.0": ">=0.7.0",
"tough-cookie@<4.1.3": ">=4.1.3"
"tough-cookie@<4.1.3": ">=4.1.3",
"nanoid@<3.3.8": ">=3.3.8"
}
}
}

33
web/pnpm-lock.yaml generated
View File

@@ -17,6 +17,7 @@ overrides:
qs@<6.2.4: '>=6.2.4'
cookie@<0.7.0: '>=0.7.0'
tough-cookie@<4.1.3: '>=4.1.3'
nanoid@<3.3.8: '>=3.3.8'
importers:
@@ -96,7 +97,7 @@ importers:
specifier: ^0.11.0
version: 0.11.2(svelte@4.2.19)
postcss:
specifier: ^8.4.32
specifier: ^8.4.49
version: 8.4.49
postcss-load-config:
specifier: ^5.0.2
@@ -107,6 +108,12 @@ importers:
svelte-check:
specifier: ^3.6.0
version: 3.8.6(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.49))(postcss@8.4.49)(svelte@4.2.19)
svelte-inview:
specifier: ^4.0.4
version: 4.0.4(svelte@4.2.19)
svelte-reveal:
specifier: ^1.1.0
version: 1.1.0
svelte-youtube-embed:
specifier: ^0.3.3
version: 0.3.3(svelte@4.2.19)
@@ -1092,9 +1099,9 @@ packages:
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
nanoid@5.0.9:
resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==}
engines: {node: ^18 || >=20}
hasBin: true
node-releases@2.0.18:
@@ -1398,6 +1405,11 @@ packages:
peerDependencies:
svelte: ^3.19.0 || ^4.0.0
svelte-inview@4.0.4:
resolution: {integrity: sha512-PlXNSHHijvQ6MhmSYUj6cMyS+39NttoTffk7W5WYF8T2tsyLgJnKhyuAF+/hSujbY0vpsmqXaMd2nolEKvR8Kw==}
peerDependencies:
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^5.0.0-next
svelte-preprocess@5.1.4:
resolution: {integrity: sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==}
engines: {node: '>= 16.0.0'}
@@ -1435,6 +1447,9 @@ packages:
typescript:
optional: true
svelte-reveal@1.1.0:
resolution: {integrity: sha512-TnLZy7UOj14ZGsbKjZUK/c+aCnPl1sieZ1VrmKPCtG1Cw9XDUslpV/KJGqJeo7sVNVeG7QnH1asrsugogR3YYg==}
svelte-youtube-embed@0.3.3:
resolution: {integrity: sha512-g4+53JauVB+Jwz486lVqW8PUTnoP1SinECWhmQSCaKuh6zrYiAOFRpzMG6vW1TaQfSigiLWyUactFWipo4lsfQ==}
peerDependencies:
@@ -2513,7 +2528,7 @@ snapshots:
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.7: {}
nanoid@5.0.9: {}
node-releases@2.0.18: {}
@@ -2610,7 +2625,7 @@ snapshots:
postcss@8.4.49:
dependencies:
nanoid: 3.3.7
nanoid: 5.0.9
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -2877,6 +2892,10 @@ snapshots:
dependencies:
svelte: 4.2.19
svelte-inview@4.0.4(svelte@4.2.19):
dependencies:
svelte: 4.2.19
svelte-preprocess@5.1.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.49))(postcss@8.4.49)(svelte@4.2.19)(typescript@5.6.3):
dependencies:
'@types/pug': 2.0.10
@@ -2890,6 +2909,8 @@ snapshots:
postcss-load-config: 5.1.0(jiti@1.21.6)(postcss@8.4.49)
typescript: 5.6.3
svelte-reveal@1.1.0: {}
svelte-youtube-embed@0.3.3(svelte@4.2.19):
dependencies:
svelte: 4.2.19

2
web/src/app.d.ts vendored
View File

@@ -3,7 +3,7 @@
// and what to do when importing types
declare namespace App {
// interface Locals {}
interface PageData {}
// interface PageData {}
// interface Error {}
// interface Platform {}
}

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import ChatInput from "./ChatInput.svelte";
import ChatMessages from "./ChatMessages.svelte";
import ModelConfig from "./ModelConfig.svelte";
import Models from "./Models.svelte";
import Patterns from "./Patterns.svelte";
</script>
<div class="flex-1 mx-auto p-4 min-h-screen">
<div class="grid grid-cols-1 auto-fit lg:grid-cols-[250px_minmax(250px,_1.5fr)_minmax(250px,_1.5fr)] gap-4 h-[calc(100vh-2rem)]">
<div class="flex flex-col space-y-1 order-3 lg:order-1">
<div class="space-y-2 max-w-full">
<div class="flex flex-col gap-2">
<Patterns />
<Models />
<ModelConfig />
</div>
</div>
</div>
<div class="flex flex-col space-y-4 order-2 lg:order-2">
<ChatInput />
</div>
<div class="flex flex-col border rounded-lg bg-muted/50 p-4 order-1 lg:order-3 max-h-[695px]">
<ChatMessages />
</div>
</div>
</div>

View File

@@ -1,30 +1,44 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Textarea } from "$lib/components/ui/textarea";
// import { Label } from "$lib/components/ui/label";
import { sendMessage, messageStore } from '$lib/store/chat';
import { systemPrompt } from '$lib/types/chat/patterns';
import { getToastStore } from '@skeletonlabs/skeleton';
import { FileButton } from '@skeletonlabs/skeleton';
import { Paperclip, Send } from 'lucide-svelte';
import { sendMessage } from '$lib/store/chat';
// import { currentSession, setSession } from '$lib/store/chat';
import { systemPrompt } from '$lib/types/chat/patterns';
import { onMount } from 'svelte';
let userInput = "";
let files: FileList;
const toastStore = getToastStore();
async function handleSubmit() {
if (!userInput.trim()) return;
try {
await sendMessage($systemPrompt.trim() + userInput.trim());
const trimmedInput = userInput.trim();
const trimmedSystemPrompt = $systemPrompt.trim();
// Clear input before sending to improve perceived performance
userInput = "";
await sendMessage(trimmedSystemPrompt + trimmedInput);
} catch (error) {
console.error('Chat submission error:', error);
toastStore.trigger({
message: 'Failed to send message. Please try again.',
background: 'variant-filled-error'
});
}
}
/* function handleSetSession(name: string | null) {
setSession(name);
} */
// 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);
@@ -34,9 +48,8 @@
<div class="flex flex-col gap-2">
<div class="flex-none">
<Textarea
value={$systemPrompt}
on:input={(e) => $systemPrompt}
bind:value={$systemPrompt}
on:input={(e) => $systemPrompt || ''}
placeholder="Enter system instructions..."
class="min-h-[330px] resize-none bg-background"
/>
@@ -46,25 +59,12 @@
<Textarea
bind:value={userInput}
on:input={(e) => userInput}
on:keydown={handleKeydown}
placeholder="Enter your message..."
class="min-h-[350px] resize-none bg-background"
/>
<div class="absolute bottom-5 right-2 gap-2 flex justify-end end-7">
<!-- TODO: Session Management. Move this to a new component, possibly ChatMessages -->
<!-- <div class="flex gap-2">
<button type="button" class="btn btn-sm variant-soft-tertiary"
on:click={() => handleSetSession(null)}
>
Clear Session
</button>
{#if !$currentSession}
<button type="button" class="btn btn-sm variant-glass-tertiary"
on:click={() => handleSetSession('new-session-' + Date.now())}
>
New Session
</button>
{/if}
</div> -->
<FileButton
name="file-upload"
button="btn btn-sm variant-soft-surface"
@@ -76,7 +76,7 @@
>
<Paperclip class="w-4" />
</FileButton>
<Button type="button" name="submit" class="btn btn-sm variant-filled-secondary" on:click={handleSubmit}>
<Button type="button" name="submit" variant="secondary" on:click={handleSubmit}>
<Send class="w-4 h-4" />
</Button>
</div>
@@ -87,4 +87,4 @@
.flex-col {
min-height: 0;
}
</style>
</style>

View File

@@ -0,0 +1,139 @@
<script lang="ts">
import { chatState, errorStore, streamingStore } from '$lib/store/chat';
import { afterUpdate } from 'svelte';
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) {
if (!isAssistant) return content;
try {
return marked.parse(content);
} catch (error) {
console.error('Error rendering markdown:', error);
return content;
}
}
</script>
<div class="chat-messages-wrapper flex flex-col h-full">
<div class="flex justify-between items-center mb-4 flex-none">
<span class="text-sm font-medium">Chat History</span>
<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}
<div class="messages-container" bind:this={messagesContainer}>
<div class="messages-content">
{#each $chatState.messages as message}
<div
class="message-item {message.role === 'assistant' ? 'pl-4' : 'font-medium'}"
transition:fade
>
<div class="message-header flex items-center gap-2 mb-1">
<span class="text-xs border rounded-lg p-1 variant-glass-secondary font-bold uppercase">{message.role}</span>
{#if message.role === 'assistant' && $streamingStore}
<span class="loading-indicator">
<span class="dot">.</span>
<span class="dot">.</span>
<span class="dot">.</span>
</span>
{/if}
</div>
{#if message.role === 'assistant'}
<div class="prose prose-sm text-inherit 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;
padding-right: 1rem;
}
.messages-content {
display: flex;
flex-direction: column;
gap: 1.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>

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import { Label } from "$lib/components/ui/label";
import { Slider } from "$lib/components/ui/slider";
import { modelConfig } from "$lib/store/model-store";
import Transcripts from "./Transcripts.svelte";
</script>
<div class="space-y-1">
<Label>Maximum Length ({$modelConfig.maxLength})</Label>
<Slider
bind:value={$modelConfig.maxLength}
min={1}
max={4000}
step={1}
/>
</div>
<div class="space-y-1">
<Label>Temperature ({$modelConfig.temperature.toFixed(1)})</Label>
<Slider
bind:value={$modelConfig.temperature}
min={0}
max={2}
step={0.1}
/>
</div>
<div class="space-y-1">
<Label>Top P ({$modelConfig.top_p.toFixed(2)})</Label>
<Slider
bind:value={$modelConfig.top_p}
min={0}
max={1}
step={0.01}
/>
</div>
<div class="space-y-1">
<Label>Frequency Penalty ({$modelConfig.frequency.toFixed(2)})</Label>
<Slider
bind:value={$modelConfig.frequency}
min={0}
max={1}
step={0.01}
/>
</div>
<div class="space-y-1">
<Label>Presence Penalty ({$modelConfig.presence.toFixed(2)})</Label>
<Slider
bind:value={$modelConfig.presence}
min={0}
max={1}
step={0.01}
/>
</div>
<div class="space-y-1">
<Transcripts />
</div>

View File

@@ -0,0 +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";
onMount(async () => {
await loadAvailableModels();
});
</script>
<div class="min-w-0">
<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>
</div>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Select } from "$lib/components/ui/select";
import { patterns, patternAPI } from "$lib/store/pattern-store";
let selectedPreset = "";
$: if (selectedPreset) {
console.log('Pattern selected:', selectedPreset);
patternAPI.selectPattern(selectedPreset);
}
onMount(async () => {
await patternAPI.loadPatterns();
});
</script>
<div class="min-w-0">
<Select
bind:value={selectedPreset}
>
<option value="">Load a pattern...</option>
{#each $patterns as pattern}
<option value={pattern.Name}>{pattern.Description}</option>
{/each}
</Select>
</div>

View File

@@ -0,0 +1,68 @@
<script lang="ts">
import { onMount } from 'svelte';
import { RotateCcw, Trash2, Save, Copy, File as FileIcon } from 'lucide-svelte';
import { sessions, sessionAPI } from '$lib/store/sessions';
import { chatState, clearMessages, revertLastMessage, currentSession, messageStore } from '$lib/store/chat';
import { Button } from '$lib/components/ui/button';
import { toastService } from '$lib/services/toast-service';
let sessionsList: string[] = [];
$: sessionName = $currentSession;
$: if ($sessions) {
sessionsList = $sessions.map(s => s.Name);
}
onMount(async () => {
try {
await sessionAPI.loadSessions();
} catch (error) {
console.error('Failed to load sessions:', error);
}
});
async function saveSession() {
try {
await sessionAPI.exportToFile($chatState.messages);
} catch (error) {
console.error('Failed to save session:', error);
}
}
async function loadSession() {
try {
const messages = await sessionAPI.importFromFile();
messageStore.set(messages);
} catch (error) {
console.error('Failed to load session:', error);
}
}
async function copyToClipboard() {
try {
await navigator.clipboard.writeText($chatState.messages.map(m => m.content).join('\n'));
toastService.success('Chat copied to clipboard!');
} catch (err) {
toastService.error('Failed to copy transcript');
}
}
</script>
<div class="space-y-4">
<div class="flex gap-2">
<Button variant="outline" size="icon" aria-label="Revert Last Message" on:click={revertLastMessage}>
<RotateCcw class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" aria-label="Clear Chat" on:click={clearMessages}>
<Trash2 class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" aria-label="Copy Chat" on:click={copyToClipboard}>
<Copy class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" aria-label="Load Session" on:click={loadSession}>
<FileIcon class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" aria-label="Save Session" on:click={saveSession}>
<Save class="h-4 w-4" />
</Button>
</div>
</div>

View File

@@ -82,8 +82,7 @@
disabled={loading}
/>
<Button
class="btn btn-sm variant-outline-tertiary variant-glass-success"
variant="outline"
variant="secondary"
on:click={fetchTranscript}
disabled={loading || !url}
>

View File

@@ -10,7 +10,7 @@
</p>
<nav class="flex items-center gap-4 ">
<BuyMeCoffee url="https://www.buymeacoffee.com/johnconnor.sec" /> <!-- And here -->
<BuyMeCoffee url="https://www.buymeacoffee.com/" /> <!-- And here -->
</nav>
</div>
</footer>

View File

@@ -23,7 +23,7 @@
const navItems = [
{ href: '/', label: 'Home' },
{ href: '/posts', label: 'Posts' },
{ href: '/tags', label: 'Tags' },
// { href: '/tags', label: 'Tags' },
{ href: '/chat', label: 'Chat' },
//{ href: '/obsidian', label: 'Obsidian' },
{ href: '/contact', label: 'Contact' },
@@ -40,7 +40,7 @@
<div class="container flex h-16 items-center justify-between px-4">
<div class="flex items-center gap-4">
<Avatar
src="/src/lib/images/fabric-logo.png"
src="/fabric-logo.png"
width="w-10"
rounded="rounded-full"
class="border-2 border-primary/20"

View File

@@ -1,4 +1,4 @@
import type { Particle } from '$lib/types/particle';
import type { Particle } from '$lib/types/interfaces/particle';
import { generateGradientColor } from '$lib/utils/colors';
export class ParticleSystem {

View File

@@ -98,7 +98,7 @@
});
</script>
<div class="pt-20 pb-8 px-4">
<div class="pt-2 pb-8 px-4">
<div class="container mx-auto max-w-4xl">
<div class="terminal-window backdrop-blur-sm">
<!-- Terminal header -->

View File

@@ -7,7 +7,7 @@ const buttonVariants = {
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm",
outline: "border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 rounded-full border variant-glass-secondary shadow-sm",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import { cn } from "$lib/types/utils";
import { cn } from "$lib/utils/utils";
let className = undefined;
export { className as class };
</script>
<label
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
"p-1 text-sm font-bold leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import { toastStore } from '$lib/store/toast-store';
import { fly } from 'svelte/transition';
import { onMount } from 'svelte';
import type { ToastMessage } from '$lib/store/toast-store';
export let toast: ToastMessage;
const TOAST_TIMEOUT = 3000;
onMount(() => {
const timer = setTimeout(() => {
toastStore.remove(toast.id);
}, TOAST_TIMEOUT);
return () => clearTimeout(timer);
});
</script>
<div
class="fixed bottom-4 right-4 p-4 rounded-lg shadow-lg"
class:bg-green-100={toast.type === 'success'}
class:bg-red-100={toast.type === 'error'}
class:bg-blue-100={toast.type === 'info'}
transition:fly={{ y: 200, duration: 300 }}
>
<p
class:text-green-800={toast.type === 'success'}
class:text-red-800={toast.type === 'error'}
class:text-blue-800={toast.type === 'info'}
>
{toast.message}
</p>
</div>

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import { toastStore } from '$lib/store/toast-store';
import Toast from './Toast.svelte';
</script>
<div class="fixed bottom-0 right-0 p-4 space-y-2 z-50">
{#each $toastStore as toast (toast.id)}
<Toast {toast} />
{/each}
</div>

View File

@@ -1,10 +1,9 @@
---
title: Getting Started with SvelteKit
aliases: [SvelteKit for Beginners]
date: 2024-11-01
---
# Getting Started with SvelteKit
SvelteKit is a framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing.
## Why SvelteKit?

View File

@@ -35,7 +35,8 @@ Obsidian is a powerful knowledge base that works on top of a local folder of pla
## Links
[[Resources]]
[[Timeline]]
[[Team Members]]
[[TeamMembers]]
```
### Why Use Obsidian?

View File

@@ -1,9 +1,12 @@
---
title: SkeletonUI
aliases: [Getting Started with SkeletonUI]
description: A comprehensive UI toolkit for SvelteKit
tags:
- svelte
- styling
- skeletonui
- CSS
date: 2023-01-17
---
SkeletonUI is a comprehensive UI toolkit that integrates seamlessly with SvelteKit and Tailwind CSS, enabling developers to build adaptive and accessible web interfaces efficiently.
@@ -223,4 +226,4 @@ For a full list of utilities and their usage, explore the SkeletonUI utilities d
This cheat sheet provides a foundational overview to help you start integrating SkeletonUI into your SvelteKit projects. For more detailed information and advanced features, please refer to the official SkeletonUI documentation.
https://www.skeleton.dev/docs/introduction
https://www.skeleton.dev/docs/introduction

View File

@@ -0,0 +1,281 @@
---
title: JSON to Markdown
aliases: [Delete this file]
description: Delete this file
date: 2024-01-17
tags: [Delete this file]
author: me
---
1 min read
SvelteKit offers powerful tools for rendering Markdown content from JSON responses, with mdsvex emerging as a popular solution for seamlessly integrating Markdown processing into Svelte applications.
## Streaming JSON to Markdown
To render Markdown content from streaming JSON in a SvelteKit application, you can combine SvelteKit's server-side rendering (SSR) capabilities with tools like mdsvex or svelte-markdown. This approach ensures that dynamic data fetched from APIs can be transformed into rich, interactive content.
Heres how you can handle streaming JSON and convert it into Markdown:
Fetch the Streaming JSON: Use SvelteKit's load function to fetch the API data. If the API streams JSON, ensure you parse it incrementally using the ReadableStream interface in JavaScript.
Parse and Extract Markdown: Once you receive the JSON chunks, extract the Markdown strings from the relevant fields. For example:
```javascript
const response = await fetch('https://api.example.com/stream');
const reader = response.body.getReader();
let markdownContent = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
markdownContent += new TextDecoder().decode(value);
}
```
Render Markdown with mdsvex or svelte-markdown:
Using mdsvex: Compile the Markdown string into HTML at runtime. Mdsvex provides a compile function for this purpose12.
```javascript
import { compile } from 'mdsvex';
const { code } = await compile(markdownContent);
```
You can then inject this compiled HTML into your Svelte component.
Using svelte-markdown: This library directly renders Markdown strings as Svelte components, making it ideal for runtime rendering3. Install it with:
```text
npm install svelte-markdown
```
Then use it in your component:
```text
import Markdown from 'svelte-markdown';
let markdownContent = '# Example Heading\nThis is some text.';
{markdownContent}
```
Optimize Streaming: If the JSON contains large amounts of data, consider rendering partial content as it arrives. SvelteKit's support for streaming responses allows you to send initial HTML while continuing to process and append additional content45.
This method is particularly useful for applications that rely on real-time data or external content sources like headless CMSs or GitHub repositories. By leveraging tools like mdsvex and svelte-markdown, you can transform raw JSON data into visually engaging Markdown content without sacrificing performance or interactivity678.
dev.to favicon
stackoverflow.com favicon
npmjs.com favicon
8 sources
## Using mdsvex for Markdown
Mdsvex is a powerful preprocessor that extends Svelte's capabilities to seamlessly integrate Markdown content into SvelteKit applications1. It allows developers to write Markdown files that can contain Svelte components, effectively blending the simplicity of Markdown with the dynamic features of Svelte2.
To set up mdsvex in a SvelteKit project:
Install mdsvex and its dependencies:
text
npm install mdsvex
Configure mdsvex in your svelte.config.js file:
```javascript
import { mdsvex } from 'mdsvex';
const config = {
extensions: ['.svelte', '.md'],
preprocess: [
mdsvex({
extensions: ['.md']
})
]
};
```
This configuration allows you to use .md files as Svelte components3.
Mdsvex offers several advantages for handling Markdown in SvelteKit:
It supports frontmatter, allowing you to include metadata at the top of your Markdown files4.
You can use Svelte components directly within your Markdown content, enabling interactive elements2.
Code highlighting is built-in, making it easy to display formatted code snippets2.
For dynamic content, such as Markdown stored in a database or fetched from an API, you can use mdsvex to render Markdown strings at runtime:
```javascript
import { compile } from 'mdsvex';
const markdownString = '# Hello, World!';
const { code } = await compile(markdownString);
```
This approach allows you to process Markdown content on-the-fly, which is particularly useful when working with content management systems or external data sources5.
By leveraging mdsvex, SvelteKit developers can create rich, interactive content experiences that combine the ease of writing in Markdown with the power of Svelte components, making it an excellent choice for blogs, documentation sites, and content-heavy applications6.
## More Markdown Integration Examples
Dynamic Markdown Rendering: For scenarios where Markdown content is dynamically fetched from an external API, you can use the marked library to parse the Markdown into HTML directly within a Svelte component. This approach is simple and effective for runtime rendering:
```text
import { marked } from 'marked';
let markdownContent = '';
async function fetchMarkdown() {
const response = await fetch('https://api.example.com/markdown');
markdownContent = await response.text();
}
$: htmlContent = marked(markdownContent);
Loading...
{@html htmlContent}
```
This method ensures that even dynamically loaded Markdown content is rendered efficiently, making it ideal for live data scenarios12.
Markdown with Frontmatter: Mdsvex supports frontmatter, which allows you to embed metadata in your Markdown files. This is particularly useful for blogs or documentation sites. For example:
```text
---
title: "My Blog Post"
date: "2024-01-01"
tags: ["svelte", "markdown"]
---
```
# Welcome to My Blog
This is a post about integrating Markdown with SvelteKit.
You can access this metadata in your Svelte components, enabling features like dynamic page titles or tag-based filtering34.
Interactive Charts in Markdown: Combine the power of Markdown with Svelte's interactivity by embedding components like charts. For instance, using Mdsvex, you can include a chart directly in your Markdown file:
```text
# Sales Data
```
Here, Chart is a Svelte component that renders a chart using libraries like Chart.js or D3.js. This approach makes it easy to create visually rich content while keeping the simplicity of Markdown56.
Custom Styling for Markdown Content: To apply consistent styles to your rendered Markdown, wrap it in a container with scoped CSS. For example:
```text
.markdown-content h1 {
color: blue;
}
.markdown-content p {
font-size: 1.2rem;
}
```
This ensures your Markdown content adheres to your application's design system without affecting other parts of the UI12.
Pagination for Large Markdown Files: If you're dealing with extensive Markdown content, split it into smaller sections and implement pagination. For example, store each section in an array and render only the current page:
```text
let currentPage = 0;
const markdownPages = [
'# Page 1\nThis is the first page.',
'# Page 2\nThis is the second page.',
'# Page 3\nThis is the third page.'
];
Previous
Next
{@html marked(markdownPages[currentPage])}
```
This approach improves performance and user experience by loading content incrementally72.
## Interactive Markdown Examples
Building on the previous examples, let's explore some more advanced techniques for integrating Markdown in SvelteKit applications:
Syntax Highlighting: Enhance code blocks in your Markdown content with syntax highlighting using libraries like Prism.js. Here's how you can set it up with mdsvex:
```javascript
import { mdsvex } from 'mdsvex';
import prism from 'prismjs';
const config = {
extensions: ['.svelte', '.md'],
preprocess: [
mdsvex({
highlight: {
highlighter: (code, lang) => {
return `${prism.highlight(code, prism.languages[lang], lang)}`;
}
}
})
]
};
```
This configuration will automatically apply syntax highlighting to code blocks in your Markdown files1.
Custom Components for Markdown Elements: Create custom Svelte components to replace standard Markdown elements. For instance, you can create a custom Image component for enhanced image handling:
```text
export let src;
export let alt;
```
Then, configure mdsvex to use this component:
```javascript
import Image from './Image.svelte';
const config = {
extensions: ['.svelte', '.md'],
preprocess: [
mdsvex({
layout: {
_: './src/layouts/DefaultLayout.svelte'
},
remarkPlugins: [],
rehypePlugins: [],
components: {
img: Image
}
})
]
};
```
This setup allows you to add lazy loading, responsive images, or other custom behaviors to all images in your Markdown content2.
Table of Contents Generation: Automatically generate a table of contents for your Markdown files using remark plugins:
```javascript
import { mdsvex } from 'mdsvex';
import remarkToc from 'remark-toc';
const config = {
extensions: ['.svelte', '.md'],
preprocess: [
mdsvex({
remarkPlugins: [
[remarkToc, { tight: true }]
]
})
]
};
```
This configuration will automatically generate a table of contents based on the headings in your Markdown files3.
Live Markdown Editor: Create an interactive Markdown editor with real-time preview:
```text
import { marked } from 'marked';
let markdownInput = '# Live Markdown Editor\n\nStart typing...';
$: htmlOutput = marked(markdownInput);
{@html htmlOutput}
```
This component allows users to input Markdown and see the rendered HTML output in real-time, which can be useful for comment systems or content management interfaces4
.
These examples demonstrate the flexibility and power of integrating Markdown with SvelteKit, enabling developers to create rich, interactive content experiences tailored to their specific needs.

View File

@@ -2,7 +2,7 @@
title: Using Markdown in Svelte
description: Learn how to use your markdown documents in Svelte Applications!
date: 2023-12-22
tags: [markdown, svelte,web-dev, documentation]
tags: [markdown, svelte, web-dev, docs, learn]
---
[Mdsvex](https://mdsvex.pngwn.io/docs#install-it)

View File

@@ -1,12 +1,13 @@
---
title: Welcome to Your Blog
description: First post on my new SvelteKit blog
aliases: [Your First Post]
description: First post on your new SvelteKit blog
date: 2024-01-17
tags: [welcome, blog]
tags: [welcome, blog, create, explore]
---
This is the first post of your new blog, powered by [SvelteKit](/posts/getting-started), [Obsidian](/obsidian), and [Fabric](/about). I'm excited to share this project with you, and I hope you find it useful for your own writing and experiences.
This part of the application is edited in <a href="http://localhost:5173/obsidian" name="Obsidian">Obsidian</a>.
This part of the application is edited in <a href="http://localhost:5173/posts/obsidian" name="Obsidian">Obsidian</a>.
## What to Expect

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

View File

@@ -14,11 +14,26 @@ export const posts: Post[] = Object.entries(modules).map(([path, module]: [strin
return {
slug,
title: module.metadata?.title || slug,
aliases: module.metadata?.aliases || [],
date: module.metadata?.date || new Date().toISOString().split('T')[0],
description: module.metadata?.description || '',
tags: module.metadata?.tags || [],
updated: module.metadata?.updated || new Date().toISOString(),
author: module.metadata?.author || '',
lead: module.metadata?.lead || '',
reference: module.metadata?.reference || '',
content: module.default
};
});
export async function getPost(slug: string) {
return posts.find(p => p.slug === slug) || null;
}
const post = posts.find(p => p.slug === slug);
if (!post) return null;
if (typeof post.content === 'string') {
const compiled = await compile(post.content);
post.content = compiled?.code || post.content;
}
return post;
}

View File

@@ -0,0 +1,168 @@
import type {
ChatRequest,
StreamResponse,
ChatError as IChatError,
ChatPrompt
} from '$lib/types/interfaces/chat-interface';
import { get } from 'svelte/store';
import { modelConfig } from '$lib/store/model-store';
import { systemPrompt } from '$lib/store/pattern-store';
import { chatConfig } from '$lib/store/chat-config';
import { messageStore } from '$lib/store/chat'; // Import messageStore
export class ChatError extends Error implements IChatError {
constructor(
message: string,
public readonly code: string = 'CHAT_ERROR',
public readonly details?: unknown
) {
super(message);
this.name = 'ChatError';
}
}
export class ChatService {
private async fetchStream(request: ChatRequest): Promise<ReadableStream<StreamResponse>> {
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new ChatError(
`HTTP error! status: ${response.status}`,
'HTTP_ERROR',
{ status: response.status }
);
}
const reader = response.body?.getReader();
if (!reader) {
throw new ChatError('Response body is null', 'NULL_RESPONSE');
}
return this.createMessageStream(reader);
} catch (error) {
if (error instanceof ChatError) {
throw error;
}
throw new ChatError(
'Failed to fetch chat stream',
'FETCH_ERROR',
error
);
}
}
private createMessageStream(reader: ReadableStreamDefaultReader<Uint8Array>): ReadableStream<StreamResponse> {
let buffer = '';
return new ReadableStream({
async start(controller) {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += new TextDecoder().decode(value);
const messages = buffer
.split('\n\n')
.filter(msg => msg.startsWith('data: '));
if (messages.length > 1) {
buffer = messages.pop() || '';
for (const msg of messages) {
controller.enqueue(JSON.parse(msg.slice(6)) as StreamResponse);
}
}
}
if (buffer.startsWith('data: ')) {
controller.enqueue(JSON.parse(buffer.slice(6)) as StreamResponse);
}
} catch (error) {
controller.error(new ChatError(
'Error processing stream',
'STREAM_PROCESSING_ERROR',
error
));
} finally {
reader.releaseLock();
controller.close();
}
},
cancel() {
reader.cancel();
}
});
}
private createChatPrompt(userInput: string, systemPromptText?: string): ChatPrompt {
const config = get(modelConfig);
return {
userInput,
systemPrompt: systemPromptText ?? get(systemPrompt),
model: config.model,
patternName: ''
};
}
public async createChatRequest(userInput: string, systemPromptText?: string): Promise<ChatRequest> {
const prompt = this.createChatPrompt(userInput, systemPromptText);
const config = get(chatConfig);
const messages = get(messageStore);
return {
prompts: [prompt],
messages: messages,
...config
};
}
public async streamChat(userInput: string, systemPromptText?: string): Promise<ReadableStream<StreamResponse>> {
const request = await this.createChatRequest(userInput, systemPromptText);
return this.fetchStream(request);
}
public async processStream(
stream: ReadableStream<StreamResponse>,
onContent: (content: string) => void,
onError: (error: Error) => void
): Promise<void> {
const reader = stream.getReader();
let accumulatedContent = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value.type === 'error') {
throw new ChatError(value.content, 'STREAM_CONTENT_ERROR');
}
accumulatedContent += value.content;
onContent(accumulatedContent);
}
} catch (error) {
if (error instanceof ChatError) {
onError(error);
} else {
onError(new ChatError(
'Error processing stream content',
'STREAM_PROCESSING_ERROR',
error
));
}
} finally {
reader.releaseLock();
}
}
}

View File

@@ -0,0 +1,17 @@
import { toastStore } from '$lib/store/toast-store';
const toastStoreInstance = toastStore;
export const toastService = {
success(message: string) {
toastStoreInstance.success(message);
},
error(message: string) {
toastStoreInstance.error(message);
},
info(message: string) {
toastStoreInstance.info(message);
}
};

View File

@@ -0,0 +1,24 @@
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
};
export const chatConfig = writable<ChatConfig>(defaultConfig);
export function updateConfig(newConfig: Partial<ChatConfig>): void {
chatConfig.update(config => ({
...config,
...newConfig
}));
}
export function resetConfig(): void {
chatConfig.set(defaultConfig);
}
export { type ChatConfig };

View File

@@ -1,14 +1,52 @@
import { writable, get } from 'svelte/store';
import type { ChatRequest, StreamResponse, ChatState, Message } from '$lib/types/interfaces/chat-interface';
import { chatApi } from '$lib/types/chat/chat';
import { modelConfig } from './model-config';
import { systemPrompt } from '$lib/types/chat/patterns';
import { writable, derived, get } from 'svelte/store';
import type { ChatState, Message } from '$lib/types/interfaces/chat-interface';
import { ChatService, ChatError } from '$lib/services/ChatService';
// Initialize chat service
const chatService = new ChatService();
// Local storage key for persisting messages
const MESSAGES_STORAGE_KEY = 'chat_messages';
// Load initial messages from local storage
const initialMessages = typeof localStorage !== 'undefined'
? JSON.parse(localStorage.getItem(MESSAGES_STORAGE_KEY) || '[]')
: [];
// Separate stores for different concerns
export const messageStore = writable<Message[]>(initialMessages);
export const streamingStore = writable<boolean>(false);
export const errorStore = writable<string | null>(null);
export const currentSession = writable<string | null>(null);
export const chatState = writable<ChatState>({
messages: [],
isStreaming: false
});
// Subscribe to messageStore changes to persist messages
if (typeof localStorage !== 'undefined') {
messageStore.subscribe($messages => {
localStorage.setItem(MESSAGES_STORAGE_KEY, JSON.stringify($messages));
});
}
// Derived store for chat state
export const chatState = derived(
[messageStore, streamingStore],
([$messages, $streaming]) => ({
messages: $messages,
isStreaming: $streaming
})
);
// Error handling utility
function handleError(error: Error | string) {
const errorMessage = error instanceof ChatError
? `${error.code}: ${error.message}`
: error instanceof Error
? error.message
: error;
errorStore.set(errorMessage);
streamingStore.set(false);
return errorMessage;
}
export const setSession = (sessionName: string | null) => {
currentSession.set(sessionName);
@@ -18,112 +56,66 @@ export const setSession = (sessionName: string | null) => {
};
export const clearMessages = () => {
chatState.update(state => ({ ...state, messages: [] }));
messageStore.set([]);
errorStore.set(null);
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(MESSAGES_STORAGE_KEY);
}
};
export const revertLastMessage = () => {
chatState.update(state => ({
...state,
messages: state.messages.slice(0, -1)
}));
messageStore.update(messages => messages.slice(0, -1));
};
export async function sendMessage(userInput: string, systemPromptText?: string) {
// Guard against streaming state
const currentState = get(chatState);
if (currentState.isStreaming) {
console.log('Message submission blocked - already streaming');
return;
}
// Update chat state
chatState.update((state) => ({
...state,
messages: [...state.messages, { role: 'user', content: userInput }],
isStreaming: true
}));
try {
const config = get(modelConfig);
const sessionName = get(currentSession);
const request: ChatRequest = {
prompts: [{
userInput: userInput,
systemPrompt: systemPromptText || get(systemPrompt),
model: Array.isArray(config.model) ? config.model.join(',') : config.model,
vendor: '',
patternName: '',
}],
temperature: config.temperature,
top_p: config.top_p,
frequency_penalty: 0,
presence_penalty: 0
};
const stream = await chatApi.streamChat(request);
const reader = stream.getReader();
let assistantMessage: Message = {
role: 'assistant',
content: ''
};
let isCancelled = false;
while (!isCancelled) {
const { done, value } = await reader.read();
if (done) break;
// Check if we're still streaming before processing
const currentState = get(chatState);
if (!currentState.isStreaming) {
isCancelled = true;
break;
}
const response = value as StreamResponse;
switch (response.type) {
case 'content':
assistantMessage.content += response.content += `\n`;
chatState.update(state => ({
...state,
messages: [
...state.messages.slice(0, -1),
{...assistantMessage}
]
}));
break;
case 'error':
throw new Error(response.content);
case 'complete':
break;
}
const $streaming = get(streamingStore);
if ($streaming) {
throw new ChatError('Message submission blocked - already streaming', 'STREAMING_BLOCKED');
}
if (isCancelled) {
throw new Error('Stream cancelled');
}
streamingStore.set(true);
errorStore.set(null);
// Add user message
messageStore.update(messages => [...messages, { role: 'user', content: userInput }]);
const stream = await chatService.streamChat(userInput, systemPromptText);
await chatService.processStream(
stream,
(content) => {
messageStore.update(messages => {
const newMessages = [...messages];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage?.role === 'assistant') {
lastMessage.content = content;
} else {
newMessages.push({
role: 'assistant',
content
});
}
return newMessages;
});
},
(error) => {
handleError(error);
}
);
streamingStore.set(false);
} catch (error) {
console.error('Chat error:', error);
// Only add error message if still streaming
const currentState = get(chatState);
if (currentState.isStreaming) {
chatState.update(state => ({
...state,
messages: [...state.messages, {
role: 'assistant',
content: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`
}]
}));
if (error instanceof Error) {
handleError(error);
} else {
handleError(String(error));
}
} finally {
chatState.update(state => ({
...state,
isStreaming: false
}));
throw error;
}
}
export type { StreamResponse };
// Re-export types for convenience
export type { ChatState, Message };

View File

@@ -1,58 +0,0 @@
import { writable, derived } from 'svelte/store';
import { modelsApi } from '$lib/types/chat/models';
import { configApi } from '$lib/types/chat/config';
import type { VendorModel } from '$lib/types/interfaces/model-interface';
import type { ModelConfig } from '$lib/types/interfaces/model-interface';
export const modelConfig = writable<ModelConfig>({
model: [],
temperature: 0.7,
maxLength: 2000,
top_p: 0.9,
frequency: 1
});
export const availableModels = writable<VendorModel[]>([]);
// Initialize available models
export async function loadAvailableModels() {
try {
const models = await modelsApi.getAvailable();
console.log('Load models:', models);
const uniqueModels = [...new Map(models.map(model => [model.name, model])).values()];
availableModels.set(uniqueModels);
} catch (error) {
console.error('Client failed to load available models:', error);
availableModels.set([]);
}
}
// Initialize config
export async function initializeConfig() {
try {
const config = await configApi.get();
const safeConfig: ModelConfig = {
...config,
model: Array.isArray(config.model) ? config.model :
typeof config.model === 'string' ? (config.model as string).split(',') : []
};
modelConfig.set(safeConfig);
} catch (error) {
console.error('Failed to load config:', error);
}
}
/* modelConfig.subscribe(async (config) => {
try {
const configRecord: Record<string, string> = {
model: config.model.toString(),
temperature: config.temperature.toString(),
maxLength: config.maxLength.toString(),
top_p: config.top_p.toString(),
frequency: config.frequency.toString()
};
// await configApi.update(configRecord);
} catch (error) {
console.error('Failed to update config:', error);
}
}); */

View File

@@ -0,0 +1,38 @@
import { writable} from 'svelte/store';
import { modelsApi } from '$lib/types/chat/models';
import { configApi } from '$lib/types/chat/config';
import type { VendorModel, ModelConfig } from '$lib/types/interfaces/model-interface';
export const modelConfig = writable<ModelConfig>({
model: '',
temperature: 0.7,
maxLength: 2000,
top_p: 0.9,
frequency: 0.5,
presence: 0
});
export const availableModels = writable<VendorModel[]>([]);
// Initialize available models
export async function loadAvailableModels() {
try {
const models = await modelsApi.getAvailable();
console.log('Load models:', models);
const uniqueModels = [...new Map(models.map(model => [model.name, model])).values()];
availableModels.set(uniqueModels);
} catch (error) {
console.error('Client failed to load available models:', error);
availableModels.set([]);
}
}
// Initialize config
export async function initializeConfig() {
try {
const config = await configApi.get();
modelConfig.set(config);
} catch (error) {
console.error('Failed to load config:', error);
}
}

View File

@@ -1,4 +1,4 @@
import { createStorageAPI } from './base';
import { createStorageAPI } from '../types/chat/base';
import type { Pattern } from '$lib/types/interfaces/pattern-interface';
import { get, writable } from 'svelte/store';

View File

@@ -0,0 +1,86 @@
import { createStorageAPI } from '../types/chat/base';
import type { Session } from '../types/interfaces/session-interface';
import type { Message } from '../types/interfaces/chat-interface';
import { get, writable } from 'svelte/store';
import { openFileDialog, readFileAsJson, saveToFile } from '../utils/file-utils';
import { toastService } from '../services/toast-service';
export const sessions = writable<Session[]>([]);
export const sessionAPI = {
...createStorageAPI<Session>('sessions'),
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/${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;
} 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;
}
}
};

View File

@@ -0,0 +1,30 @@
import { writable } from 'svelte/store';
export interface ToastMessage {
message: string;
type: 'success' | 'error' | 'info';
id: number;
}
function createToastStore() {
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));
}
};
}
export const toastStore = createToastStore();

View File

@@ -1,4 +1,3 @@
// import type { ModelConfig } from '$lib/types/model-types';
import type { StorageEntity } from '$lib/types/interfaces/storage-interface';
interface APIErrorResponse {
@@ -13,53 +12,30 @@ interface APIResponse<T> {
// Define and export the base api object
export const api = {
async fetch<T>(endpoint: string, options: RequestInit = {}): Promise<APIResponse<T>> {
try {
const response = await fetch(`/api${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
...options.headers,
},
});
if (!response.ok) {
const errorData = await response.json() as APIErrorResponse;
return { error: errorData.error || response.statusText };
}
const data = await response.json();
return { data };
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error occurred' };
}
},
get: <T>(fetch: typeof window.fetch, endpoint: string) => api.fetch<T>(endpoint),
post: <T>(fetch: typeof window.fetch, endpoint: string, data: unknown) =>
api.fetch<T>(endpoint, {
method: 'POST',
body: JSON.stringify(data),
}),
put: <T>(fetch: typeof window.fetch, endpoint: string, data?: unknown) =>
api.fetch<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
}),
delete: <T>(fetch: typeof window.fetch, endpoint: string) =>
api.fetch<T>(endpoint, {
method: 'DELETE',
}),
stream: async function*(fetch: typeof window.fetch, endpoint: string, data: unknown): AsyncGenerator<string> {
const response = await fetch(`/api${endpoint}`, {
method: 'POST',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
return { error: (await response.json() as APIErrorResponse).error || response.statusText };
}
return { data: await response.json() as T };
},
get: <T>(endpoint: string) => api.fetch<T>(endpoint),
post: <T>(endpoint: string, data: unknown) => api.fetch<T>(endpoint, { method: 'POST', body: JSON.stringify(data) }),
put: <T>(endpoint: string, data?: unknown) => api.fetch<T>(endpoint, { method: 'PUT', body: data ? JSON.stringify(data) : undefined }),
delete: <T>(endpoint: string) => api.fetch<T>(endpoint, { method: 'DELETE' }),
stream: async function* (endpoint: string, data: unknown): AsyncGenerator<string> {
const response = await fetch(`/api${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
@@ -73,57 +49,48 @@ export const api = {
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield decoder.decode(value);
if (done) break;
}
}
};
export function createStorageAPI<T extends StorageEntity>(entityType: string) {
return {
// Get a specific entity by name
async get(name: string): Promise<T> {
const response = await api.fetch<T>(`/api${entityType}/${name}`);
const response = await api.fetch<T>(`/${entityType}/${name}`);
if (response.error) throw new Error(response.error);
return response.data as T;
},
// Get all entity names
async getNames(): Promise<string[]> {
const response = await api.fetch<string[]>(`/api${entityType}/names`);
const response = await api.fetch<string[]>(`/${entityType}/names`);
if (response.error) throw new Error(response.error);
return response.data || [];
return response.data as [];
},
// Delete an entity
async delete(name: string): Promise<void> {
const response = await api.fetch(`/api${entityType}/${name}`, {
method: 'DELETE',
});
const response = await api.fetch(`/${entityType}/${name}`, { method: 'DELETE' });
if (response.error) throw new Error(response.error);
},
// Check if an entity exists
async exists(name: string): Promise<boolean> {
const response = await api.fetch<boolean>(`/api${entityType}/exists/${name}`);
const response = await api.fetch<boolean>(`/${entityType}/exists/${name}`);
if (response.error) throw new Error(response.error);
return response.data || false;
return response.data as boolean;
},
// Rename an entity
async rename(oldName: string, newName: string): Promise<void> {
const response = await api.fetch(`/api${entityType}/rename/${oldName}/${newName}`, {
method: 'PUT',
});
const response = await api.fetch(`/${entityType}/rename/${oldName}/${newName}`, { method: 'PUT' });
if (response.error) throw new Error(response.error);
},
// Save an entity
async save(name: string, content: string | object): Promise<void> {
const body = typeof content === 'string' ? content : JSON.stringify(content);
const response = await api.fetch(`/api${entityType}/${name}`, {
const response = await api.fetch(`/${entityType}/${name}`, {
method: 'POST',
body,
body: JSON.stringify(content),
headers: { 'Content-Type': 'application/json' },
});
if (response.error) throw new Error(response.error);
},

View File

@@ -1,59 +0,0 @@
// import { api } from '$lib/types/base';
import type { ChatRequest, StreamResponse } from '$lib/types/interfaces/chat-interface';
// Create a chat API client
export const chatApi = {
async streamChat(request: ChatRequest): Promise<ReadableStream<StreamResponse>> {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('Response body is null');
let buffer = '';
return new ReadableStream({
async start(controller) {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += new TextDecoder().decode(value);
const messages = buffer
.split('\n\n')
.filter(msg => msg.startsWith('data: '));
// Process complete messages
if (messages.length > 1) {
// Keep the last (potentially incomplete) chunk in the buffer
buffer = messages.pop() || '';
for (const msg of messages) {
controller.enqueue(JSON.parse(msg.slice(6)) as StreamResponse);
}
}
}
} catch (error) {
controller.error(error);
} finally {
// Process any remaining complete messages in the buffer
if (buffer.startsWith('data: ')) {
controller.enqueue(JSON.parse(buffer.slice(6)) as StreamResponse);
}
controller.close();
reader.releaseLock();
}
},
});
},
};

View File

@@ -1,25 +1,27 @@
import type { ModelConfig } from '$lib/types/interfaces/model-interface';
import { api } from './base';
export const configApi = {
async get(): Promise<ModelConfig> {
const response = await api.fetch<ModelConfig>('/config');
if (response.error) throw new Error(response.error);
return response.data || {
model: [],
temperature: 0.7,
top_p: 0.9,
frequency: 1,
maxLength: 2000
};
},
/* async update(config: Record<string, string>) {
const response = await api.fetch('config/update', {
method: 'POST',
body: JSON.stringify(config),
});
if (response.error) throw new Error(response.error);
return response;
} */
const DEFAULT_CONFIG: Omit<ModelConfig, 'model'> = {
temperature: 0.7,
top_p: 0.9,
frequency: .5,
presence: 0,
maxLength: 2000
};
export const configApi = {
async get(): Promise<ModelConfig> {
try {
const response = await api.fetch<ModelConfig>('/config');
if (!response.data) {
return { ...DEFAULT_CONFIG, model: '' };
}
return response.data;
} catch (error) {
console.error('Failed to fetch config:', error);
throw error;
}
}
};

View File

@@ -3,38 +3,22 @@ import type { VendorModel, ModelsResponse } from '$lib/types/interfaces/model-in
export const modelsApi = {
async getAvailable(): Promise<VendorModel[]> {
const response = await api.fetch<ModelsResponse>('/models/names');
console.log("Client raw API response:", response)
if (response.error) {
console.error("Client couldn't fetch models:", response.error);
throw new Error(response.error);
}
if (!response.data) {
console.error('No data received from models API');
return [];
}
const vendorsData = response.data.vendors || {};
const result: VendorModel[] = [];
for (const [vendor, models] of Object.entries(vendorsData)) {
for (const model of models) {
result.push({
name: model,
vendor: vendor
});
try {
const response = await api.fetch<ModelsResponse>('/models/names');
if (!response.data?.vendors) {
throw new Error('Invalid response format: missing vendors data');
}
return Object.entries(response.data.vendors).flatMap(([vendor, models]) =>
models.map(model => ({
name: model,
vendor
}))
);
} catch (error) {
console.error("Failed to fetch models:", error);
throw error;
}
console.log('Available models:', result);
return result;
},
/* async getNames(): Promise<string[]> {
const response = await api.fetch<ModelsResponse>('/models/names');
if (response.error) throw new Error(response.error);
return response.data?.models || [];
} */
};

View File

@@ -1,4 +0,0 @@
import { createStorageAPI } from './base';
import type { Session } from '$lib/types/interfaces/session-interface';
export const sessionAPI = createStorageAPI<Session>('sessions');

View File

@@ -1,21 +1,32 @@
export interface ChatRequest {
prompts: {
userInput: string;
systemPrompt: string;
model: string;
vendor: string;
// contextName: string;
patternName: string;
// sessionName: string;
}[];
export type MessageRole = 'system' | 'user' | 'assistant';
export type ResponseFormat = 'markdown' | 'mermaid' | 'plain';
export type ResponseType = 'content' | 'error' | 'complete';
export interface ChatPrompt {
userInput: string;
systemPrompt: string;
model: string;
patternName: string;
}
export interface ChatConfig {
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;
}
export interface Message {
role: 'system' | 'user' | 'assistant';
role: MessageRole;
content: string;
}
@@ -23,9 +34,15 @@ export interface ChatState {
messages: Message[];
isStreaming: boolean;
}
export interface StreamResponse {
type: 'content' | 'error' | 'complete';
format: 'markdown' | 'mermaid' | 'plain';
export interface StreamResponse {
type: ResponseType;
format: ResponseFormat;
content: string;
}
export interface ChatError {
code: string;
message: string;
details?: unknown;
}

View File

@@ -10,15 +10,10 @@ export interface ModelsResponse {
export interface ModelConfig {
model: string[];
model: string;
temperature: number;
top_p: number;
maxLength: number;
frequency: number;
presence: number;
}
/* export type ModelSelect = {
vendor: string;
name: string;
}*/

View File

@@ -1,6 +1,7 @@
import type { Message } from "$lib/types/interfaces/chat-interface";
import type { Message } from "./chat-interface";
export interface Session {
name: string;
content: Message[];
Name: string;
Message: string | Message[];
Session: string | object;
}

View File

@@ -1,5 +1,7 @@
export interface StorageEntity {
Name: string;
Description: string;
Pattern: string | object;
Description?: string;
Pattern?: string | object;
Session?: string | object;
Context?: string | object;
}

View File

@@ -1,10 +0,0 @@
export interface Post {
title: string;
date: string;
description: string;
tags: string[];
}
export interface PostMetadata extends Post {
slug: string;
}

View File

@@ -1,56 +0,0 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
// import { cubicOut } from 'svelte/easing';
// import type { TransitionConfig } from 'svelte/transition';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/* type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (style: Record<string, number | string | undefined>): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, '');
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
}; */

View File

@@ -0,0 +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();
});
}
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);
});
}
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);
}

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -1,6 +1,7 @@
<script>
import '../app.postcss';
import { AppShell, Toast } from '@skeletonlabs/skeleton';
import { AppShell } from '@skeletonlabs/skeleton';
import ToastContainer from '$lib/components/ui/toast/ToastContainer.svelte';
import Footer from './Footer.svelte';
import Header from './Header.svelte';
import { initializeStores } from '@skeletonlabs/skeleton';
@@ -24,7 +25,7 @@
});
</script>
<Toast position="t" />
<ToastContainer />
{#key $page.url.pathname}
<AppShell class="relative">
@@ -35,7 +36,7 @@
<div class="h-2 py-4">
</svelte:fragment>
<div
in:fly={{ duration: 1000, delay: 300, y: 100 }}
in:fly={{ duration: 500, delay: 100, y: 100 }}
>
<main class="mx-auto p-4">
<slot />

View File

@@ -1,20 +1,15 @@
<script lang="ts">
// import Terminal from './Terminal.svelte';
import Fabric from './Fabric.svelte';
import Card from '$lib/components/ui/cards/card.svelte';
import { Youtube } from 'svelte-youtube-lite';
import Terminal from '../lib/components/home/Terminal.svelte';
import Fabric from '../lib/components/home/Fabric.svelte';
import { slide } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
let augemented = false;
let cards = false;
// let showTerminal = false;
let showTerminal = false;
setTimeout(() => {
augemented = true;
cards = true;
// showTerminal = true;
showTerminal = true;
}, 1000);
</script>
@@ -27,83 +22,23 @@
<h1 class="h1 text-8xl font-bold font-sans mt-8">
<span class="bg-gradient-to-br to-blue-500 from-cyan-300 bg-clip-text text-transparent box-decoration-clone">fabric</span>
</h1>
{#if augemented}
<div class="py-2 mb-6" transition:slide|local="{{delay: 250, duration: 3000, easing: quintOut }}">
{#if augemented}
<div class="py-2 mb-6" transition:slide|local="{{delay: 250, duration: 3000, easing: quintOut }}">
<h2 class="h2 text-2xl text-center font-extrabold bg-gradient-to-br to-blue-500 from-cyan-300 bg-clip-text text-transparent pb-2">Human Flourishing via AI Augmentation</h2>
<div class="text-2xl">
<p class="mt-2 font-bold">Fill in the blanks... </p>
<i class="mt-2 font-medium justify-end">I believe one of the biggest problems in my world is <code class="code variant-filled-secondary">___________,</code> which is why I'm building/creating/doing <code class="code variant-filled-secondary">___________.</code>
<div class="text-2xl">
<p class="mt-2 font-bold">Fill in the blanks... </p>
<i class="mt-2 font-medium justify-end">I believe one of the biggest problems in my world is <code class="code variant-filled-secondary">___________,</code> which is why I'm building/creating/doing <code class="code variant-filled-secondary">___________.</code>
</div>
</div>
</div>
{/if}
</div>
<br>
{#if cards}
<div transition:slide|local="{{delay: 2500, duration: 4000, easing: quintOut, axis: 'x'}}">
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 justify-end">
<div class="container mx-auto justify-left">
<img src="https://img.shields.io/github/languages/top/danielmiessler/fabric" alt="Github top language">
<img src="https://img.shields.io/github/last-commit/danielmiessler/fabric" alt="GitHub last commit">
<img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License">
<br>
<hr class="!border-t-4" />
<br>
<h4 class="h4"><b>Leverage Proven Patterns</b> - Use the extensive library of pre-built patterns, crafted to extract maximum value from AI interactions. From summarization to analysis, code review to creative writing, the patterns help you achieve consistent, high-quality results.<br>
</h4>
<br>
<Youtube id="UbDyjIIGaxQ" title="Network Chuck Explains fabric" />
<br>
</div>
<Card
header="Let Your Voice Be Heard"
imageUrl="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/b40c4a73-3efa-401d-a172-aa545b65f088/image.png?t=1703300882"
imageAlt="Blog post header image"
title="99% of Earth's population holds the self-limiting belief that only certain people have worthy thoughts and opinions"
content="Two ideas that belong in the past: 1) Smoking improves your health, and 2) Thinking and sharing is only for special people"
authorName="Daniel Miessler"
authorAvatarUrl="src/lib/images/dan.png"
link="https://danielmiessler.com/p/blogging-podcasting-gatekeeping-concepts"
/>
{#if showTerminal}
<div >
<Terminal />
</div>
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 mt-8">
<Card
header="Curate Your Content"
imageUrl="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/697388b0-0eeb-4854-8fb2-83651227ecc0/Dynamic-Content-Summaries-Miessler-2024.png?t=1720992541"
imageAlt="Blog post header image"
title="Dynamic Content Generation (DCG)"
content="Dynamic Content Generation will change how all media is created and consumed"
authorName="Daniel Miessler"
authorAvatarUrl="src/lib/images/dan.png"
link="https://danielmiessler.com/p/dynamic-content-summaries"
/>
<div class="container mx-auto justify-right">
<blockquote class="blockquote">There are countless use cases for AI. What will you use if for?</blockquote>
</div>
</div>
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 justify-end mt-8">
<div class="container mx-auto justify-left">
<hr class="!border-t-4" />
<br>
<h4 class="h4">Showcase your interests. Tell people what you've been working on. Create your community.</h4>
</div>
<Card
header="Explore the Possibilities"
imageUrl="https://public-files.gumroad.com/a1xibdqveljasnxffudsqdp7bqpo"
imageAlt="Blog post header image"
title="Red Blue Purple AI - December 2024"
content="A new course from Jason Haddix!"
authorName="Jason Haddix"
authorAvatarUrl="src/lib/images/haddix.jpeg"
link="https://arcanuminfosec.gumroad.com/l/ygmlpe"
/>
</div>
</div>
{/if}

View File

@@ -1,15 +1,58 @@
<script>
import Content from './README.md'
import Content from './README.md';
import { onMount } from 'svelte';
let toc = [];
onMount(() => {
// Get all headings from the content
const article = document.querySelector('article');
if (article) {
const headings = article.querySelectorAll('h1, h2, h3, h4, h5, h6');
toc = Array.from(headings).map(heading => ({
id: heading.id,
text: heading.textContent,
level: parseInt(heading.tagName.charAt(1))
}));
}
});
function scrollToSection(id) {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
</script>
{#if Content}
<article class="container max-w-3xl">
<div class="space-y-4">
<div class="items-center mx-auto grid-cols-[80%_20%] grid gap-8 max-w-7xl relative">
<article class="prose max-w-3xl flex-1">
<div class="space-y-4">
<svelte:component this={Content} />
</div>
</article>
</div>
</article>
<nav class="hidden lg:block w-64 fixed top-24 right-[max(0px,calc(50%-45rem))] max-h-[calc(80vh-5rem)] overflow-y-auto">
<div class="p-4 bg-card text-card-foreground">
<h4 class="font-semibold mb-4">On this page</h4>
<ul class="space-y-2">
{#each toc as item}
<li style="margin-left: {(item.level - 1) * 1}rem">
<a
href="#{item.id}"
class="text-xs hover:text-primary transition-colors"
on:click|preventDefault={() => scrollToSection(item.id)}
>
{item.text}
</a>
</li>
{/each}
</ul>
</div>
</nav>
</div>
{:else}
<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">
@@ -19,4 +62,9 @@
</div>
{/if}
<!-- <style>
:global(h1, h2, h3, h4, h5, h6) {
scroll-margin-top: 5rem;
}
</style> -->

View File

@@ -6,7 +6,7 @@ updated: 2024-11-22
---
The UI for Fabric can be found [here](/chat).
<div align="center">
<img src="./src/lib/images/fabric-logo.gif" alt="fabriclogo" width="200" height="200"/>
<img src="/fabric-logo.gif" alt="fabriclogo" width="200" height="200"/>
# `fabric`

View File

@@ -1,19 +1,6 @@
<script lang="ts">
import ChatInput from "./ChatInput.svelte";
import ChatMessages from "./ChatMessages.svelte";
import ModelConfig from "./ModelConfig.svelte";
import Chat from '$lib/components/chat/Chat.svelte';
</script>
<div class="flex-1 mx-auto p-4 min-h-screen">
<div class="grid grid-cols-1 lg:grid-cols-[250px_minmax(250px,_1.5fr)_minmax(250px,_1.5fr)] gap-4 h-[calc(100vh-2rem)]">
<div class="flex flex-col space-y-1 order-3 lg:order-1">
<ModelConfig />
</div>
<div class="flex flex-col space-y-4 order-2 lg:order-2">
<ChatInput />
</div>
<div class="flex flex-col border rounded-lg bg-muted/50 p-4 order-1 lg:order-3 max-h-[695px]">
<ChatMessages />
</div>
</div>
</div>
<Chat />

View File

@@ -1,221 +0,0 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { RotateCcw, Trash2, Save, Copy } from 'lucide-svelte';
import { chatState, clearMessages, revertLastMessage, currentSession } from '$lib/store/chat';
import { afterUpdate } from 'svelte';
import { marked } from 'marked';
import { getToastStore } from '@skeletonlabs/skeleton';
import { Toast } from '@skeletonlabs/skeleton';
let sessionName: string | null = null;
let messagesContainer: HTMLDivElement;
currentSession.subscribe(value => {
sessionName = value;
});
afterUpdate(() => {
if (messagesContainer) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
});
function saveChat() {
const chatData = JSON.stringify($chatState.messages, null, 2);
const blob = new Blob([chatData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'chat-history.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
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;
}
}
const toastStore = getToastStore();
async function copyToClipboard() {
try {
await navigator.clipboard.writeText($chatState.messages.map(m => m.content).join('\n'));
toastStore.trigger({
message: 'Chat copied to clipboard!',
background: 'variant-filled-success'
});
} catch (err) {
toastStore.trigger({
message: 'Failed to copy transcript',
background: 'variant-filled-error'
});
}
}
</script>
<div class="chat-messages-wrapper flex flex-col h-full">
<div class="flex justify-between items-center mb-4 flex-none">
<span class="text-sm font-medium">Chat History</span>
<div class="flex gap-2">
<Button class="variant-glass-tertiary" variant="outline" size="icon" aria-label="Revert Last Message" on:click={revertLastMessage}>
<RotateCcw class="h-4 w-4" />
</Button>
<Button class="variant-glass-tertiary" variant="outline" size="icon" aria-label="Clear Chat" on:click={clearMessages}>
<Trash2 class="h-4 w-4" />
</Button>
<Button class="variant-glass-tertiary" variant="outline" size="icon" aria-label="Copy Chat" on:click={copyToClipboard}>
<Copy class="h-4 w-4" />
</Button>
<Toast position="b" />
<Button class="variant-glass-tertiary" variant="outline" size="icon" aria-label="Save Chat" on:click={saveChat}>
<Save class="h-4 w-4" />
</Button>
</div>
</div>
<div class="messages-container" bind:this={messagesContainer}>
<div class="messages-content">
{#each $chatState.messages as message}
<div class="message-item {message.role === 'assistant' ? 'pl-4' : 'font-medium'} transition-all">
<span class="text-xs tertiary uppercase">{message.role}:</span>
{#if message.role === 'assistant'}
{@html renderMarkdown(message.content, true)}
{:else}
<div class="whitespace-pre-wrap text-sm">
{message.content}
</div>
{/if}
</div>
{/each}
{#if $chatState.isStreaming}
<div class="pl-4 text-tertiary-700 animate-pulse"></div>
{/if}
</div>
</div>
</div>
<style>
.chat-messages-wrapper {
display: flex;
flex-direction: column;
min-height: 0;
}
.messages-container {
position: relative;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--color-primary-500) transparent;
}
.messages-content {
padding-bottom: 1rem;
padding-right: 0.5rem;
}
.message-item {
margin-bottom: 0.5rem;
}
.messages-container::-webkit-scrollbar {
width: 2px;
}
.messages-container::-webkit-scrollbar-track {
background: transparent;
}
.messages-container::-webkit-scrollbar-thumb {
background-color: var(--color-primary-500);
border-radius: 2px;
}
/* Markdown content styles */
:global(.message-item.pl-4) {
font-size: 0.875rem;
}
:global(.message-item pre) {
background-color: rgb(50, 50, 50);
padding: 1rem;
border-radius: 0.5rem;
margin: 0.5rem 0;
overflow-x: auto;
}
:global(.message-item code) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.875rem;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
background-color: rgb(50, 50, 60);
}
:global(.message-item h1) {
margin: 0.5rem 0;
font: bold 1.5rem/1.5 system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
:global(.message-item h2) {
margin: 0.5rem 0;
font: bold 1.25rem/1.5 system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
:global(.message-item h3) {
margin: 0.5rem 0;
font: bold 1rem/1.5 system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
:global(.message-item h4) {
margin: 0.5rem 0;
font: bold 0.875rem/1.5 system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
:global(.message-item h5) {
margin: 0.5rem 0;
font: bold 0.75rem/1.5 system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
:global(.message-item h6) {
margin: 0.5rem 0;
font: bold 0.625rem/1.5 system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
:global(.message-item p) {
margin: 0.5rem 0;
}
:global(.message-item ul, .message-item ol) {
margin: 0.5rem 0;
padding-left: 1.5rem;
}
:global(.message-item li) {
margin: 0.25rem 0;
}
:global(.message-item a) {
color: rgb(var(--color-primary-600));
text-decoration: underline;
}
:global(.message-item blockquote) {
border-left: 4px solid rgb(var(--color-secondary-200));
margin: 0.5rem 0;
padding-left: 1rem;
font-style: italic;
}
</style>

View File

@@ -1,91 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Select } from "$lib/components/ui/select";
import { Label } from "$lib/components/ui/label";
import { Slider } from "$lib/components/ui/slider";
import { modelConfig, availableModels, loadAvailableModels } from "$lib/store/model-config";
import Transcripts from "./Transcripts.svelte";
import { patterns } from "$lib/types/chat/patterns";
import { patternAPI } from "$lib/types/chat/patterns";
let selectedPreset = "";
$: if (selectedPreset) {
console.log('Pattern selected:', selectedPreset);
patternAPI.selectPattern(selectedPreset);
}
onMount(async () => {
await loadAvailableModels();
await patternAPI.loadPatterns();
});
</script>
<div class="space-y-2 max-w-full">
<div class="flex flex-col gap-2">
<div class="min-w-0">
<Select
bind:value={selectedPreset}
>
<option value="">Load a pattern...</option>
{#each $patterns as pattern}
<option value={pattern.Name}>{pattern.Description}</option>
{/each}
</Select>
</div>
<div class="min-w-0">
<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>
</div>
</div>
<div class="space-y-1">
<Label class="p-1 font-bold">Temperature ({$modelConfig.temperature.toFixed(1)})</Label>
<Slider
bind:value={$modelConfig.temperature}
min={0}
max={2}
step={0.1}
/>
</div>
<div class="space-y-1">
<Label class="p-1 font-bold">Maximum Length ({$modelConfig.maxLength})</Label>
<Slider
bind:value={$modelConfig.maxLength}
min={1}
max={4000}
step={1}
/>
</div>
<div class="space-y-1">
<Label class="p-1 font-bold">Top P ({$modelConfig.top_p.toFixed(2)})</Label>
<Slider
bind:value={$modelConfig.top_p}
min={0}
max={1}
step={0.01}
/>
</div>
<div class="space-y-1">
<Label class="p-1 font-bold">Frequency Penalty ({$modelConfig.frequency.toFixed(2)})</Label>
<Slider
bind:value={$modelConfig.frequency}
min={0}
max={1}
step={0.01}
/>
</div>
<div class="space-y-1">
<Transcripts />
</div>
</div>

View File

@@ -1,76 +0,0 @@
<!-- WIP to be included in ChatInput or ChatHeader -->
<!--
<script lang="ts">
import { onMount } from 'svelte';
import { sessionAPI } from '$lib/types/chat/sessions';
import { currentSession } from '$lib/store/chat';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
let sessions: string[] = [];
let newSessionName = '';
onMount(async () => {
try {
sessions = await sessionAPI.getNames();
} catch (error) {
console.error('Failed to load sessions:', error);
}
});
async function createSession() {
if (!newSessionName.trim()) return;
try {
await sessionAPI.save(newSessionName, JSON.stringify({
name: newSessionName,
messages: []
}));
sessions = await sessionAPI.getNames();
currentSession.set(newSessionName);
newSessionName = '';
} catch (error) {
console.error('Failed to create session:', error);
}
}
async function deleteSession(name: string) {
try {
await sessionAPI.delete(name);
sessions = await sessionAPI.getNames();
if ($currentSession === name) {
currentSession.set(null);
}
} catch (error) {
console.error('Failed to delete session:', error);
}
}
</script>
<div class="space-y-4">
<div class="flex gap-2">
<Input bind:value={newSessionName} placeholder="New session name" />
<Button on:click={createSession}>Create</Button>
</div>
<div class="space-y-2">
{#each sessions as session}
<div class="flex items-center justify-between p-2 border rounded">
<button
class="flex-1 text-left"
on:click={() => currentSession.set(session)}
>
{session}
</button>
<Button
variant="destructive"
size="sm"
on:click={() => deleteSession(session)}
>
Delete
</Button>
</div>
{/each}
</div>
</div> -->

View File

@@ -3,30 +3,31 @@ title: "Contact"
description: "Default Contact Page"
date: 2024-11-24
---
<div class="flex flex-col items-center justify-center">
<div class="container flex flex-col items-center justify-center gap-6">
<h1 class="text-3xl font-bold tracking-tight text-white sm:text-5xl"><i>{title}</i></h1>
<script lang="ts">
import { Contact } from 'lucide-svelte';
</script>
> **This is a placeholder contact page. No logic is implemented here.**
<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="text-center text-muted-foreground md:text-lg">
{description}
</p>
</div>
Place your contact info here: ...
<div class="flex items-center space-x-4">
<a href="https://github.com/johnconnor-sec" class="inline-block">
<img src="https://github.com/johnconnor-sec.png" alt="John Connor" class="w-12 h-12 rounded-full">
</a>
<a href="https://x.com/Noob73286788366" class="inline-block">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-twitter-x" viewBox="0 0 16 16">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z"/>
</svg>
</a>
<a href="https://medium.com/@j0hnc0nn0r" class="inline-block">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-medium" viewBox="0 0 16 16">
<path d="M9.025 8c0 2.485-2.02 4.5-4.513 4.5A4.506 4.506 0 0 1 0 8c0-2.486 2.02-4.5 4.512-4.5A4.506 4.506 0 0 1 9.025 8m4.95 0c0 2.34-1.01 4.236-2.256 4.236S9.463 10.339 9.463 8c0-2.34 1.01-4.236 2.256-4.236S13.975 5.661 13.975 8M16 8c0 2.096-.355 3.795-.794 3.795-.438 0-.793-1.7-.793-3.795 0-2.096.355-3.795.794-3.795.438 0 .793 1.699.793 3.795"/>
</svg>
</a>
<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>

View File

@@ -1,26 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
</script>
<div class="container flex min-h-[400px] flex-col items-center justify-center">
<div class="text-center">
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl">
{$page.status}: {$page.error?.message || 'Something went wrong'}
</h1>
<p class="mt-4 text-muted-foreground">
{#if $page.status === 404}
Sorry, we couldn't find the posts you're looking for.
{:else}
An error occurred while loading the posts. Please try again later.
{/if}
</p>
<div class="mt-8">
<a
href="/posts"
class="inline-flex items-center justify-center rounded-md bg-primary px-8 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
Try Again
</a>
</div>
</div>
</div>

View File

@@ -1,23 +0,0 @@
<script lang="ts">
import Content from './Obsidian.md'
</script>
{#if Content}
<article class="container max-w-3xl">
<div class="space-y-4">
<svelte:component this={Content} />
</div>
</article>
{:else}
<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>
</div>
</div>
{/if}

View File

@@ -1,5 +0,0 @@
import { dev } from '$app/environment';
export const csr = dev;
export const prerender = true;

View File

@@ -1,31 +1,164 @@
<script lang="ts">
import { formatDistance } from 'date-fns';
import type { PageData } from './$types';
// import { Paginator } from '@skeletonlabs/skeleton'
// import Spinner from '$lib/components/ui/spinner/spinner.svelte';
import Card from '$lib/components/ui/cards/card.svelte';
import { Youtube } from 'svelte-youtube-lite';
import { slide } from 'svelte/transition';
import { elasticOut, quintOut } from 'svelte/easing';
import { InputChip } from '@skeletonlabs/skeleton';
let cards = false;
let searchQuery = '';
let selectedTags: string[] = [];
let allTags: string[] = [];
export let data: PageData;
$: posts = data.posts;
// Extract all unique tags from posts
$: {
const tagSet = new Set<string>();
posts.forEach(post => {
post.meta.tags.forEach(tag => tagSet.add(tag));
});
allTags = Array.from(tagSet);
}
// Filter posts based on selected tags
$: filteredPosts = posts.filter(post => {
if (selectedTags.length === 0) return true;
return selectedTags.every(tag =>
post.meta.tags.some(postTag => postTag.toLowerCase() === tag.toLowerCase())
);
});
function validateTag(value: string): boolean {
return allTags.some(tag => tag.toLowerCase() === value.toLowerCase());
}
let visible: boolean = true;
</script>
<div class="container py-12">
<h1 class="mb-4 text-3xl font-bold">Blog Posts</h1>
<p class="text-sm mb-8 font-small">This blog is maintained in an Obsidian Vault</p>
{#if posts.length === 0}
{#if !visible}
<aside class="alert variant-ghost">
<div>(icon)</div>
<slot:fragment href="./+error.svelte" />
<div class="alert-actions">(buttons)</div>
</aside>
{/if}
<p class="text-sm mb-4 font-small">This blog is maintained in an Obsidian Vault</p>
<div >
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 justify-end">
<div class="container mx-auto justify-left">
<img src="https://img.shields.io/github/languages/top/danielmiessler/fabric" alt="Github top language">
<img src="https://img.shields.io/github/last-commit/danielmiessler/fabric" alt="GitHub last commit">
<img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License">
<br>
<hr class="!border-t-4" />
<br>
<h4 class="h4"><b>Leverage Proven Patterns</b></h4>
<br>
<Youtube id="UbDyjIIGaxQ" title="Network Chuck Explains fabric" />
<p>Post your favorite videos.</p>
<br>
</div>
<div>
<h4 class="h4"><b>Share Your Most Important Toughts and Ideas</b></h4>
<br>
<Card
header="Let Your Voice Be Heard"
imageUrl="/brain.png"
imageAlt="Blog post header image"
title="Blogging, Podcasting, Videos, and More."
content="What will you create?"
authorName="Your Name Here"
authorAvatarUrl=""
link="/"
/>
</div>
</div>
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 mt-8">
<Card
header="Curate Your Content"
imageUrl="/electric.png"
imageAlt="Blog post header image"
title="Enter a new title here"
content="What will you share"
authorName="Your Name Here"
authorAvatarUrl=""
link="/"
/>
<div class="container mx-auto justify-right">
<blockquote class="blockquote">There are countless use cases for AI. What will you use if for?</blockquote>
</div>
</div>
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 justify-end mt-8 pb-8">
<div class="container mx-auto justify-left">
<hr class="!border-t-4" />
<br>
<h4 class="h4">Showcase your interests. Tell people what you've been working on. Create your community.</h4>
</div>
<Card
header="Explore the Possibilities"
imageUrl=""
imageAlt="Blog post header image"
title="Enter a new title here"
content="What will you share?"
authorName="Your Name Here"
authorAvatarUrl=""
link="/"
/>
</div>
</div>
<!-- Tag search and filter section -->
<div class="mb-6">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<InputChip
bind:value={selectedTags}
name="tags"
placeholder="Search and press Enter to add tags..."
validation={validateTag}
allowDuplicates={false}
class="input"
/>
<div class="tags-container overflow-x-auto pb-2">
<div class="flex gap-2">
{#each allTags.filter(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())) as tag}
<button
class="tag-button px-3 py-1 rounded-full text-sm font-medium transition-colors
{selectedTags.includes(tag.toLowerCase())
? 'bg-primary text-primary-foreground'
: 'bg-secondary hover:bg-secondary/80'}"
on:click={() => {
const tagLower = tag.toLowerCase();
if (!selectedTags.includes(tagLower)) {
selectedTags = [...selectedTags, tagLower];
}
searchQuery = '';
}}
>
{tag}
</button>
{/each}
</div>
</div>
</div>
</div>
</div>
{#if filteredPosts.length === 0}
{#if !visible}
<aside class="alert variant-ghost">
<div>(icon)</div>
<slot:fragment href="./+error.svelte" />
<div class="alert-actions">(buttons)</div>
</aside>
{/if}
{:else}
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{#each posts as post}
{#each filteredPosts as post}
<article class="card card-hover group relative rounded-lg border p-6 hover:bg-muted/50">
<a href="/posts/{post.slug}" class="absolute inset-0">
<span class="sr-only">View {post.meta.title}</span>
@@ -60,4 +193,30 @@
<!-- <Paginator records={posts} limit={6} buttonClass="btn" /> -->
</div>
{/if}
</div>
</div>
<style>
.tags-container {
scrollbar-width: thin;
scrollbar-color: var(--color-primary) transparent;
}
.tags-container::-webkit-scrollbar {
height: 6px;
}
.tags-container::-webkit-scrollbar-track {
background: transparent;
}
.tags-container::-webkit-scrollbar-thumb {
background-color: var(--color-primary);
border-radius: 6px;
}
.tag-button {
white-space: nowrap;
}
</style>

View File

@@ -1,6 +1,5 @@
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
// import type { PostMetadata } from '$lib/types';
export const load: PageLoad = async () => {
try {
@@ -25,7 +24,7 @@ export const load: PageLoad = async () => {
aliases: post.metadata?.aliases || [],
lead: post.metadata?.lead || '',
updated: post.metadata?.updated || new Date().toISOString(),
author: post.metadata?.author || 'John Connor',
author: post.metadata?.author || 'Your Name Here',
}
};
});

View File

@@ -1,12 +1,10 @@
<script lang="ts">
import { formatDistance } from 'date-fns';
import type { PageData } from './$types';
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
export let data: PageData;
$: ({ content: Content, meta } = data);
$: formattedDate = formatDistance(new Date(meta.date), new Date(), { addSuffix: true });
</script>
<article class="container max-w-3xl py-6 lg:py-2">
@@ -18,24 +16,10 @@
</div>
</div>
{:then Content}
<!-- <div class="space-y-4">
<div class="space-y-4">
<h1 class="inline-block text-4xl font-bold lg:text-5xl">{meta.title}</h1>
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
<time datetime={meta.date}>{formattedDate}</time>
<span class="text-xs">•</span>
<div class="flex items-center space-x-2">
{#each meta.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>
</div>
</div>
<hr class="my-8" /> -->
<div class="prose prose-slate dark:prose-invert">
<svelte:component this={Content} />
</div>

View File

@@ -9,6 +9,7 @@ export const load: PageLoad = async ({ params }) => {
content: post.default,
meta: {
title: post.metadata.title,
aliases: post.metadata.aliases || [],
date: post.metadata.date,
description: post.metadata.description,
tags: post.metadata.tags || [],

BIN
web/static/brain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
web/static/electric.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 MiB

View File

Before

Width:  |  Height:  |  Size: 42 MiB

After

Width:  |  Height:  |  Size: 42 MiB

View File

Before

Width:  |  Height:  |  Size: 42 MiB

After

Width:  |  Height:  |  Size: 42 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 KiB

View File

@@ -15,7 +15,7 @@ const __dirname = dirname(__filename);
const mdsvexOptions = {
extensions: ['.md', '.svx'],
layout: {
_: join(__dirname, './src/lib/layouts/post.svelte')
_: join(__dirname, './src/lib/posts-layout/post.svelte')
},
highlight: {
theme: {
@@ -44,7 +44,8 @@ const config = {
$components: join(__dirname, 'src/lib/components'),
$lib: join(__dirname, 'src/lib'),
$styles: join(__dirname, 'src/styles'),
$stores: join(__dirname, 'src/lib/stores'),
$content: join(__dirname, 'src/content'),
$utils: join(__dirname, 'src/lib/utils')
}
},

View File

@@ -68,8 +68,8 @@ export default {
'50%': { 'background-size': '200% 200%, background-position: right center' },
},
blink: {
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0 },
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0' },
},
},
typography: {
@@ -88,7 +88,6 @@ export default {
},
plugins: [
forms,
typography,
skeleton({
themes: {

View File

@@ -9,7 +9,8 @@
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"module": "es2022",
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files

View File

@@ -12,6 +12,12 @@ export default defineConfig({
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
},
watch: {
usePolling: true,
interval: 100,
ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**']
}
}
},
optimizeDeps: {}
});