Updates
5
go.mod
@@ -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
@@ -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=
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package main
|
||||
|
||||
var version = "v..1"
|
||||
var version = "v1.4.120"
|
||||
|
||||
4
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -3,7 +3,7 @@
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Locals {}
|
||||
interface PageData {}
|
||||
// interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
27
web/src/lib/components/chat/Chat.svelte
Normal 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>
|
||||
@@ -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>
|
||||
139
web/src/lib/components/chat/ChatMessages.svelte
Normal 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>
|
||||
60
web/src/lib/components/chat/ModelConfig.svelte
Normal 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>
|
||||
20
web/src/lib/components/chat/Models.svelte
Normal 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>
|
||||
27
web/src/lib/components/chat/Patterns.svelte
Normal 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>
|
||||
68
web/src/lib/components/chat/SessionManager.svelte
Normal 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>
|
||||
@@ -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}
|
||||
>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -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 {
|
||||
@@ -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 -->
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
33
web/src/lib/components/ui/toast/Toast.svelte
Normal 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>
|
||||
10
web/src/lib/components/ui/toast/ToastContainer.svelte
Normal 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>
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
@@ -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
|
||||
281
web/src/lib/content/posts/streaming-json.md
Normal 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.
|
||||
Here’s 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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 173 KiB |
@@ -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;
|
||||
}
|
||||
168
web/src/lib/services/ChatService.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
web/src/lib/services/toast-service.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
24
web/src/lib/store/chat-config.ts
Normal 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 };
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
}
|
||||
}); */
|
||||
38
web/src/lib/store/model-store.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
86
web/src/lib/store/session-store.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
30
web/src/lib/store/toast-store.ts
Normal 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();
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 || [];
|
||||
} */
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
import { createStorageAPI } from './base';
|
||||
import type { Session } from '$lib/types/interfaces/session-interface';
|
||||
|
||||
export const sessionAPI = createStorageAPI<Session>('sessions');
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}*/
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export interface Post {
|
||||
title: string;
|
||||
date: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface PostMetadata extends Post {
|
||||
slug: string;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}; */
|
||||
44
web/src/lib/utils/file-utils.ts
Normal 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);
|
||||
}
|
||||
6
web/src/lib/utils/utils.ts
Normal 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));
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
@@ -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> -->
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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> -->
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
export const csr = dev;
|
||||
|
||||
export const prerender = true;
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
After Width: | Height: | Size: 2.0 MiB |
BIN
web/static/electric.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 42 MiB |
|
Before Width: | Height: | Size: 42 MiB After Width: | Height: | Size: 42 MiB |
|
Before Width: | Height: | Size: 42 MiB After Width: | Height: | Size: 42 MiB |
|
Before Width: | Height: | Size: 798 KiB |
@@ -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')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {}
|
||||
});
|
||||