mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-09 14:28:01 -05:00
john 2024-11-26 08:53:48
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,7 +22,7 @@ dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
||||
26
web/src/lib/components/ui/button/button.svelte
Normal file
26
web/src/lib/components/ui/button/button.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/types/utils";
|
||||
import { buttonVariants } from "./index.js";
|
||||
|
||||
let className = undefined;
|
||||
export let variant = "";
|
||||
export let size = "default";
|
||||
export { className as class };
|
||||
|
||||
$: classes = cn(
|
||||
buttonVariants.base,
|
||||
buttonVariants.variants.variant[variant as keyof typeof buttonVariants.variants.variant],
|
||||
buttonVariants.variants.size[size as keyof typeof buttonVariants.variants.size],
|
||||
className
|
||||
);
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={classes}
|
||||
type="button"
|
||||
on:click
|
||||
on:keydown
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
31
web/src/lib/components/ui/button/index.js
Normal file
31
web/src/lib/components/ui/button/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import Root from "./button.svelte";
|
||||
|
||||
const buttonVariants = {
|
||||
base: "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50",
|
||||
variants: {
|
||||
variant: {
|
||||
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",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline"
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default"
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
Root,
|
||||
Root as Button,
|
||||
buttonVariants
|
||||
};
|
||||
32
web/src/lib/components/ui/buymeacoffee/BuyMeCoffee.svelte
Normal file
32
web/src/lib/components/ui/buymeacoffee/BuyMeCoffee.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
export let url: string = 'https://www.buymeacoffee.com/johnconnor.sec';
|
||||
export let text: string = 'Buy me a coffee';
|
||||
|
||||
</script>
|
||||
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm px-3 py-1.5 btn variant-filled-tertiary hover:variant-filled-secondary transition-all duration-200 flex items-center gap-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="transition-transform duration-200 group-hover:rotate-12"
|
||||
>
|
||||
<path d="M17 8h1a4 4 0 1 1 0 8h-1" />
|
||||
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z" />
|
||||
<line x1="6" y1="2" x2="6" y2="4" />
|
||||
<line x1="10" y1="2" x2="10" y2="4" />
|
||||
<line x1="14" y1="2" x2="14" y2="4" />
|
||||
</svg>
|
||||
{text}
|
||||
</a>
|
||||
6
web/src/lib/components/ui/input/index.ts
Normal file
6
web/src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import Root from "./input.svelte";
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
33
web/src/lib/components/ui/input/input.svelte
Normal file
33
web/src/lib/components/ui/input/input.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/types/utils";
|
||||
let className: string | undefined = undefined;
|
||||
export let value = undefined;
|
||||
export { className as class };
|
||||
export let readonly = undefined;
|
||||
</script>
|
||||
|
||||
<input
|
||||
class={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-lg transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{readonly}
|
||||
on:blur
|
||||
on:change
|
||||
on:click
|
||||
on:focus
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:keyup
|
||||
on:mouseover
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:mousemove
|
||||
on:paste
|
||||
on:input
|
||||
on:wheel|passive
|
||||
{...$$restProps}
|
||||
/>
|
||||
6
web/src/lib/components/ui/label/index.js
Normal file
6
web/src/lib/components/ui/label/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Root as Label
|
||||
};
|
||||
15
web/src/lib/components/ui/label/label.svelte
Normal file
15
web/src/lib/components/ui/label/label.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script>
|
||||
import { cn } from "$lib/types/utils.ts";
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
6
web/src/lib/components/ui/select/index.js
Normal file
6
web/src/lib/components/ui/select/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import Root from "./select.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Root as Select
|
||||
};
|
||||
30
web/src/lib/components/ui/select/select-content.svelte
Normal file
30
web/src/lib/components/ui/select/select-content.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script>
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
let className = undefined;
|
||||
export let sideOffset = 4;
|
||||
export let position = "popper";
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<SelectPrimitive.Content {position} {sideOffset} {...$$restProps}>
|
||||
<div
|
||||
class={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</SelectPrimitive.Content>
|
||||
</div>
|
||||
16
web/src/lib/components/ui/select/select-item.svelte
Normal file
16
web/src/lib/components/ui/select/select-item.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script>
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
let className = undefined;
|
||||
export let value;
|
||||
export let disabled = false;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<option
|
||||
{value}
|
||||
{disabled}
|
||||
class={cn("relative flex w-full cursor-default select-none py-1.5 pl-2 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className)}
|
||||
>
|
||||
<slot />
|
||||
</option>
|
||||
10
web/src/lib/components/ui/select/select-label.svelte
Normal file
10
web/src/lib/components/ui/select/select-label.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script>
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/types/utils.ts";
|
||||
let className = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Label class={cn("px-2 py-1.5 text-sm variant-filled-secondary font-semibold", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</SelectPrimitive.Label>
|
||||
8
web/src/lib/components/ui/select/select-separator.svelte
Normal file
8
web/src/lib/components/ui/select/select-separator.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script>
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/types/utils.ts";
|
||||
let className = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Separator class={cn("bg-muted -mx-1 my-1 h-px", className)} {...$$restProps} />
|
||||
20
web/src/lib/components/ui/select/select-trigger.svelte
Normal file
20
web/src/lib/components/ui/select/select-trigger.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { ChevronDown } from "lucide-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
let className = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
<ChevronDown class="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Trigger>
|
||||
18
web/src/lib/components/ui/select/select-value.svelte
Normal file
18
web/src/lib/components/ui/select/select-value.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = SelectPrimitive.ValueProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
export let placeholder: $$Props["placeholder"] = undefined;
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Value
|
||||
class={cn("text-sm", className)}
|
||||
{placeholder}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</SelectPrimitive.Value>
|
||||
24
web/src/lib/components/ui/select/select.svelte
Normal file
24
web/src/lib/components/ui/select/select.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/types/utils.ts";
|
||||
import { ChevronDown } from "lucide-svelte";
|
||||
|
||||
export let value: any = undefined;
|
||||
export let disabled = false;
|
||||
let className = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<select
|
||||
{disabled}
|
||||
bind:value
|
||||
class={cn(
|
||||
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-lg ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 appearance-none",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</select>
|
||||
<ChevronDown class="absolute right-3 top-2.5 h-4 w-4 opacity-50 pointer-events-none" />
|
||||
</div>
|
||||
6
web/src/lib/components/ui/slider/index.js
Normal file
6
web/src/lib/components/ui/slider/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import Root from "./slider.svelte";
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Slider,
|
||||
};
|
||||
89
web/src/lib/components/ui/slider/slider.svelte
Normal file
89
web/src/lib/components/ui/slider/slider.svelte
Normal file
@@ -0,0 +1,89 @@
|
||||
<script>
|
||||
import { cn } from "$lib/types/utils.ts";
|
||||
|
||||
let className = undefined;
|
||||
export let value = 0;
|
||||
export let min = 0;
|
||||
export let max = 100;
|
||||
export let step = 1;
|
||||
export { className as class };
|
||||
|
||||
let sliderEl;
|
||||
let isDragging = false;
|
||||
|
||||
$: percentage = ((value - min) / (max - min)) * 100;
|
||||
|
||||
function handleMouseDown(e) {
|
||||
isDragging = true;
|
||||
updateValue(e);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
function handleMouseMove(e) {
|
||||
if (!isDragging) return;
|
||||
updateValue(e);
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
isDragging = false;
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
function updateValue(e) {
|
||||
if (!sliderEl) return;
|
||||
const rect = sliderEl.getBoundingClientRect();
|
||||
const pos = (e.clientX - rect.left) / rect.width;
|
||||
const rawValue = min + (max - min) * pos;
|
||||
const steppedValue = Math.round(rawValue / step) * step;
|
||||
value = Math.max(min, Math.min(max, steppedValue));
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
const step = e.shiftKey ? 10 : 1;
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
value = Math.max(min, value - step);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
value = Math.min(max, value + step);
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
value = min;
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
value = max;
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={sliderEl}
|
||||
class={cn("relative flex w-full touch-none select-none items-center", className)}
|
||||
on:mousedown={handleMouseDown}
|
||||
on:keydown={handleKeyDown}
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
aria-valuenow={value}
|
||||
>
|
||||
<div class="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||
<div
|
||||
class="absolute h-full bg-primary transition-all"
|
||||
style="width: {percentage}%"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute h-4 w-4 rounded-full border border-secondary bg-primary-500 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
||||
style="left: calc({percentage}% - 0.5rem)"
|
||||
/>
|
||||
</div>
|
||||
9
web/src/lib/components/ui/spinner/spinner.svelte
Normal file
9
web/src/lib/components/ui/spinner/spinner.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/types/utils';
|
||||
import { Loader2 } from 'lucide-svelte';
|
||||
|
||||
let className: string | undefined = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<Loader2 class={cn('h-4 w-4 animate-spin', className)} {...$$restProps} />
|
||||
66
web/src/lib/components/ui/tag-list/TagList.svelte
Normal file
66
web/src/lib/components/ui/tag-list/TagList.svelte
Normal file
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cn } from '$lib/types/utils';
|
||||
|
||||
export let tags: string[] = [];
|
||||
export let tagsPerPage = 5;
|
||||
export let className: string | undefined = undefined;
|
||||
|
||||
let currentPage = 0;
|
||||
let containerWidth: number;
|
||||
|
||||
$: totalPages = Math.ceil(tags.length / tagsPerPage);
|
||||
$: startIndex = currentPage * tagsPerPage;
|
||||
$: endIndex = Math.min(startIndex + tagsPerPage, tags.length);
|
||||
$: visibleTags = tags.slice(startIndex, endIndex);
|
||||
$: canGoBack = currentPage > 0;
|
||||
$: canGoForward = currentPage < totalPages - 1;
|
||||
|
||||
function nextPage() {
|
||||
if (canGoForward) {
|
||||
currentPage++;
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (canGoBack) {
|
||||
currentPage--;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={cn('relative flex items-center gap-2', className)} bind:clientWidth={containerWidth}>
|
||||
{#if totalPages > 1 && canGoBack}
|
||||
<button
|
||||
on:click={prevPage}
|
||||
class="flex h-6 w-6 items-center justify-center rounded-md border bg-background hover:bg-muted"
|
||||
transition:slide
|
||||
>
|
||||
<ChevronLeft class="h-4 w-4" />
|
||||
<span class="sr-only">Previous page</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each visibleTags as tag (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-muted"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if totalPages > 1 && canGoForward}
|
||||
<button
|
||||
on:click={nextPage}
|
||||
class="flex h-6 w-6 items-center justify-center rounded-md border bg-background hover:bg-muted"
|
||||
transition:slide
|
||||
>
|
||||
<ChevronRight class="h-4 w-4" />
|
||||
<span class="sr-only">Next page</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
6
web/src/lib/components/ui/textarea/index.js
Normal file
6
web/src/lib/components/ui/textarea/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import Root from "./textarea.svelte";
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Textarea,
|
||||
};
|
||||
29
web/src/lib/components/ui/textarea/textarea.svelte
Normal file
29
web/src/lib/components/ui/textarea/textarea.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import { cn } from "$lib/types/utils.ts";
|
||||
let className = undefined;
|
||||
export let value = undefined;
|
||||
export { className as class };
|
||||
export let readonly = undefined;
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
class={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-lg focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{readonly}
|
||||
on:blur
|
||||
on:change
|
||||
on:click
|
||||
on:focus
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:keyup
|
||||
on:mouseover
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:paste
|
||||
on:input
|
||||
{...$$restProps}
|
||||
></textarea>
|
||||
1
web/src/lib/content/.obsidian/app.json
vendored
Normal file
1
web/src/lib/content/.obsidian/app.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/src/lib/content/.obsidian/appearance.json
vendored
Normal file
1
web/src/lib/content/.obsidian/appearance.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
30
web/src/lib/content/.obsidian/core-plugins.json
vendored
Normal file
30
web/src/lib/content/.obsidian/core-plugins.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"file-explorer": true,
|
||||
"global-search": true,
|
||||
"switcher": true,
|
||||
"graph": true,
|
||||
"backlink": true,
|
||||
"canvas": true,
|
||||
"outgoing-link": true,
|
||||
"tag-pane": true,
|
||||
"properties": false,
|
||||
"page-preview": true,
|
||||
"daily-notes": true,
|
||||
"templates": true,
|
||||
"note-composer": true,
|
||||
"command-palette": true,
|
||||
"slash-command": false,
|
||||
"editor-status": true,
|
||||
"bookmarks": true,
|
||||
"markdown-importer": false,
|
||||
"zk-prefixer": false,
|
||||
"random-note": false,
|
||||
"outline": true,
|
||||
"word-count": true,
|
||||
"slides": false,
|
||||
"audio-recorder": false,
|
||||
"workspaces": false,
|
||||
"file-recovery": true,
|
||||
"publish": false,
|
||||
"sync": false
|
||||
}
|
||||
5
web/src/lib/content/.obsidian/templates.json
vendored
Normal file
5
web/src/lib/content/.obsidian/templates.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"folder": "templates",
|
||||
"dateFormat": "YYYY-MM-DD",
|
||||
"timeFormat": "HH:mm"
|
||||
}
|
||||
8
web/src/lib/content/.obsidian/types.json
vendored
Normal file
8
web/src/lib/content/.obsidian/types.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"types": {
|
||||
"aliases": "aliases",
|
||||
"cssclasses": "multitext",
|
||||
"tags": "tags",
|
||||
"updated": "datetime"
|
||||
}
|
||||
}
|
||||
173
web/src/lib/content/.obsidian/workspace.json
vendored
Normal file
173
web/src/lib/content/.obsidian/workspace.json
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"main": {
|
||||
"id": "d2e57b203fabd791",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "bede7cd0fb75a7df",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "c588c8d12c5f7702",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "templates/{{title}}.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "{{title}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "vertical"
|
||||
},
|
||||
"left": {
|
||||
"id": "a69ef8c1dea71399",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "99030135b6260693",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "bbeb4ea8d01ce855",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "file-explorer",
|
||||
"state": {
|
||||
"sortOrder": "alphabetical"
|
||||
},
|
||||
"icon": "lucide-folder-closed",
|
||||
"title": "Files"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "afc5509c38fa5543",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "search",
|
||||
"state": {
|
||||
"query": "",
|
||||
"matchingCase": false,
|
||||
"explainSearch": false,
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical"
|
||||
},
|
||||
"icon": "lucide-search",
|
||||
"title": "Search"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "53c950f1571616a8",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "bookmarks",
|
||||
"state": {},
|
||||
"icon": "lucide-bookmark",
|
||||
"title": "Bookmarks"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 300
|
||||
},
|
||||
"right": {
|
||||
"id": "74142d853a5fb911",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "065cd0d2b52977db",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "398f4b2bc7fb48c1",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "backlink",
|
||||
"state": {
|
||||
"file": "posts/welcome.md",
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
"showSearch": false,
|
||||
"searchQuery": "",
|
||||
"backlinkCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-coming-in",
|
||||
"title": "Backlinks for welcome"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "655e694ad24637c7",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outgoing-link",
|
||||
"state": {
|
||||
"file": "posts/welcome.md",
|
||||
"linksCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-going-out",
|
||||
"title": "Outgoing links from welcome"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "eba769dfb90abcb3",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "tag",
|
||||
"state": {
|
||||
"sortOrder": "frequency",
|
||||
"useHierarchy": true
|
||||
},
|
||||
"icon": "lucide-tags",
|
||||
"title": "Tags"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2bcc1385d707df56",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"file": "posts/welcome.md"
|
||||
},
|
||||
"icon": "lucide-list",
|
||||
"title": "Outline of welcome"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 300,
|
||||
"collapsed": true
|
||||
},
|
||||
"left-ribbon": {
|
||||
"hiddenItems": {
|
||||
"switcher:Open quick switcher": false,
|
||||
"graph:Open graph view": false,
|
||||
"canvas:Create new canvas": false,
|
||||
"daily-notes:Open today's daily note": false,
|
||||
"templates:Insert template": false,
|
||||
"command-palette:Open command palette": false
|
||||
}
|
||||
},
|
||||
"active": "c588c8d12c5f7702",
|
||||
"lastOpenFiles": [
|
||||
"posts/welcome.md",
|
||||
"templates/{{title}}.md",
|
||||
"posts/SkeletonUI.md",
|
||||
"posts/getting-started.md",
|
||||
"posts/extract_wisdom.md"
|
||||
]
|
||||
}
|
||||
226
web/src/lib/content/posts/SkeletonUI.md
Executable file
226
web/src/lib/content/posts/SkeletonUI.md
Executable file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
title: SkeletonUI
|
||||
tags:
|
||||
- svelte
|
||||
- styling
|
||||
- skeletonui
|
||||
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.
|
||||
|
||||
SkeletonUI offers a comprehensive suite of components to enhance your Svelte applications. Below is a categorized list of these components, presented in Svelte syntax:
|
||||
|
||||
```svelte
|
||||
<!-- Layout Components -->
|
||||
<AppShell />
|
||||
<AppBar />
|
||||
<Sidebar />
|
||||
<Footer />
|
||||
|
||||
<!-- Navigation Components -->
|
||||
<NavMenu />
|
||||
<Breadcrumbs />
|
||||
<Tabs />
|
||||
<Pagination />
|
||||
|
||||
<!-- Form Components -->
|
||||
<Button />
|
||||
<Input />
|
||||
<Select />
|
||||
<Textarea />
|
||||
<Checkbox />
|
||||
<Radio />
|
||||
<Switch />
|
||||
<Slider />
|
||||
<FileUpload />
|
||||
|
||||
<!-- Data Display Components -->
|
||||
<Card />
|
||||
<Avatar />
|
||||
<Badge />
|
||||
<Chip />
|
||||
<Divider />
|
||||
<Table />
|
||||
<List />
|
||||
<Accordion />
|
||||
<ProgressBar />
|
||||
<Rating />
|
||||
<Tag />
|
||||
|
||||
<!-- Feedback Components -->
|
||||
<Alert />
|
||||
<Modal />
|
||||
<Toast />
|
||||
<Popover />
|
||||
<Tooltip />
|
||||
|
||||
<!-- Media Components -->
|
||||
<Image />
|
||||
<Video />
|
||||
<Icon />
|
||||
|
||||
<!-- Utility Components -->
|
||||
<Spinner />
|
||||
<SkeletonLoader />
|
||||
<Placeholder />
|
||||
```
|
||||
|
||||
For detailed information on each component, including their properties and usage examples, please refer to the official SkeletonUI documentation.
|
||||
___
|
||||
Below is an expanded cheat sheet to assist you in leveraging SkeletonUI within your SvelteKit projects.
|
||||
|
||||
**1\. Installation**
|
||||
|
||||
To set up SkeletonUI in a new SvelteKit project, follow these steps:
|
||||
|
||||
- **Create a new SvelteKit project**:
|
||||
|
||||
```bash
|
||||
npx sv create my-skeleton-app
|
||||
cd my-skeleton-app
|
||||
npm install
|
||||
```
|
||||
- **Install SkeletonUI packages**:
|
||||
|
||||
```bash
|
||||
npm install -D @skeletonlabs/skeleton@next @skeletonlabs/skeleton-svelte@next
|
||||
```
|
||||
- **Configure Tailwind CSS**:
|
||||
|
||||
In your `tailwind.config.js` file, add the following:
|
||||
|
||||
```javascript
|
||||
import { skeleton, contentPath } from '@skeletonlabs/skeleton/plugin';
|
||||
import * as themes from '@skeletonlabs/skeleton/themes';
|
||||
|
||||
export default {
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,ts}',
|
||||
contentPath(import.meta.url, 'svelte')
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
skeleton({
|
||||
themes: [themes.cerberus, themes.rose]
|
||||
})
|
||||
]
|
||||
};
|
||||
```
|
||||
- **Set the active theme**:
|
||||
|
||||
In your `src/app.html`, set the `data-theme` attribute on the `<body>` tag:
|
||||
|
||||
```html
|
||||
<body data-theme="cerberus">
|
||||
<!-- Your content -->
|
||||
</body>
|
||||
```
|
||||
|
||||
For detailed installation instructions, refer to the official SkeletonUI documentation.
|
||||
|
||||
**2\. Components**
|
||||
|
||||
SkeletonUI offers a variety of pre-designed components to accelerate your development process. Here's how to use some of them:
|
||||
|
||||
- **Button**:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Button } from '@skeletonlabs/skeleton-svelte';
|
||||
</script>
|
||||
|
||||
<Button on:click={handleClick}>Click Me</Button>
|
||||
```
|
||||
- **Card**:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Card } from '@skeletonlabs/skeleton-svelte';
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<h2>Card Title</h2>
|
||||
<p>Card content goes here.</p>
|
||||
</Card>
|
||||
```
|
||||
- **Form Input**:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Input } from '@skeletonlabs/skeleton-svelte';
|
||||
let inputValue = '';
|
||||
</script>
|
||||
|
||||
<Input bind:value={inputValue} placeholder="Enter text" />
|
||||
```
|
||||
|
||||
For a comprehensive list of components and their usage, consult the SkeletonUI components documentation.
|
||||
|
||||
**3\. Theming**
|
||||
|
||||
SkeletonUI's theming system allows for extensive customization:
|
||||
|
||||
- **Applying a Theme**:
|
||||
|
||||
Set the desired theme in your `tailwind.config.js` and `app.html` as shown in the installation steps above.
|
||||
- **Switching Themes Dynamically**:
|
||||
|
||||
To enable dynamic theme switching, you can modify the `data-theme` attribute programmatically:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button on:click={() => switchTheme('rose')}>Switch to Rose Theme</button>
|
||||
```
|
||||
- **Customizing Themes**:
|
||||
|
||||
You can create custom themes by defining your own color palettes and settings in the `tailwind.config.js` file.
|
||||
|
||||
For more information on theming, refer to the SkeletonUI theming guide.
|
||||
|
||||
**4\. Utilities**
|
||||
|
||||
SkeletonUI provides several utility functions and actions to enhance your SvelteKit application:
|
||||
|
||||
- **Table of Contents**:
|
||||
|
||||
Automatically generate a table of contents based on page headings:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { TableOfContents, tocCrawler } from '@skeletonlabs/skeleton-svelte';
|
||||
</script>
|
||||
|
||||
<div use:tocCrawler>
|
||||
<TableOfContents />
|
||||
<!-- Your content with headings -->
|
||||
</div>
|
||||
```
|
||||
- **Transitions and Animations**:
|
||||
|
||||
Utilize built-in transitions for smooth animations:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { fade } from '@skeletonlabs/skeleton-svelte';
|
||||
let visible = true;
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<div transition:fade>
|
||||
Fading content
|
||||
</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
For a full list of utilities and their usage, explore the SkeletonUI utilities documentation.
|
||||
|
||||
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
|
||||
135
web/src/lib/content/posts/Using-Markdown-in-Svelte.md
Executable file
135
web/src/lib/content/posts/Using-Markdown-in-Svelte.md
Executable file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
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]
|
||||
---
|
||||
[Mdsvex](https://mdsvex.pngwn.io/docs#install-it)
|
||||
|
||||
Here are some examples illustrating how to use Mdsvex in a Svelte application:
|
||||
|
||||
**Example 1**: Basic Markdown with Svelte Component
|
||||
Create a file named example.svx:
|
||||
|
||||
markdown
|
||||
```markdown
|
||||
---
|
||||
title: "Interactive Markdown Example"
|
||||
---
|
||||
|
||||
<script>
|
||||
import Counter from '../components/Counter.svelte';
|
||||
</script>
|
||||
|
||||
# {title}
|
||||
|
||||
This is an example of combining Markdown with a Svelte component:
|
||||
|
||||
<Counter />
|
||||
```
|
||||
|
||||
In this example:
|
||||
- The frontmatter (--- sections) defines variables like title.
|
||||
- A Svelte component Counter is imported and used inside the Markdown.
|
||||
|
||||
**Example 2**: Custom Layouts with Mdsvex
|
||||
Assuming you have a layout component at src/lib/layouts/BlogLayout.svelte:
|
||||
|
||||
svelte
|
||||
```text
|
||||
<!-- BlogLayout.svelte -->
|
||||
<script>
|
||||
export let title;
|
||||
</script>
|
||||
|
||||
<div class="blog-post">
|
||||
<h1>{title}</h1>
|
||||
<slot />
|
||||
</div>
|
||||
```
|
||||
|
||||
Now, to use this layout in your Markdown:
|
||||
**markdown**
|
||||
```markdown
|
||||
---
|
||||
title: "My Favorite Layout"
|
||||
layout: "../lib/layouts/BlogLayout.svelte"
|
||||
---
|
||||
|
||||
## Markdown with Custom Layout
|
||||
|
||||
This Markdown file will be wrapped by the `BlogLayout`.
|
||||
```
|
||||
|
||||
**Example 3:** Using Frontmatter Variables in Markdown
|
||||
**markdown**
|
||||
```markdown
|
||||
---
|
||||
author: "John Doe"
|
||||
date: "2024-11-15"
|
||||
---
|
||||
|
||||
# Blog Post
|
||||
|
||||
By {author} on {date}
|
||||
|
||||
Here's some markdown content. You can reference frontmatter values directly in the body.
|
||||
```
|
||||
|
||||
**Example 4**: Interactive Elements in Markdown
|
||||
markdown
|
||||
```markdown
|
||||
---
|
||||
title: "Interactive Chart"
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Chart } from '../components/Chart.svelte';
|
||||
</script>
|
||||
|
||||
# {title}
|
||||
|
||||
Below is an interactive chart:
|
||||
|
||||
<Chart />
|
||||
```
|
||||
|
||||
## Setting Up Mdsvex
|
||||
|
||||
To make these work, you need to configure your SvelteKit project:
|
||||
1. Install Mdsvex:
|
||||
```bash
|
||||
npm install -D mdsvex
|
||||
```
|
||||
|
||||
2. Configure SvelteKit:
|
||||
In your svelte.config.js:
|
||||
```javascript
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import { mdsvex } from 'mdsvex';
|
||||
|
||||
/** @type {import('mdsvex').MdsvexOptions} */
|
||||
const mdsvexOptions = {
|
||||
extensions: ['.svx'],
|
||||
};
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
extensions: ['.svelte', '.svx'],
|
||||
preprocess: [
|
||||
vitePreprocess(),
|
||||
mdsvex(mdsvexOptions),
|
||||
],
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
3. Create a Route for Markdown Files:
|
||||
Place your .svx files in the src/routes directory or subdirectories, and SvelteKit will automatically handle them as routes.
|
||||
|
||||
These examples show how you can integrate Mdsvex into your Svelte application, combining the simplicity of Markdown with the interactivity of Svelte components. Remember, any Svelte component you want to use within Markdown must be exported from a .svelte file and imported in your .svx file.
|
||||
66
web/src/lib/content/posts/extract_wisdom.md
Executable file
66
web/src/lib/content/posts/extract_wisdom.md
Executable file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: Extract Wisdom
|
||||
date: 2024-01-01
|
||||
description:
|
||||
tags: [patterns, fabric]
|
||||
---
|
||||
|
||||
# IDENTITY and PURPOSE
|
||||
|
||||
You extract surprising, insightful, and interesting information from text content. You are interested in insights related to the purpose and meaning of life, human flourishing, the role of technology in the future of humanity, artificial intelligence and its affect on humans, memes, learning, reading, books, continuous improvement, and similar topics.
|
||||
|
||||
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
|
||||
|
||||
# STEPS
|
||||
|
||||
- Extract a summary of the content in 25 words, including who is presenting and the content being discussed into a section called SUMMARY.
|
||||
|
||||
- Extract 20 to 50 of the most surprising, insightful, and/or interesting ideas from the input in a section called IDEAS:. If there are less than 50 then collect all of them. Make sure you extract at least 20.
|
||||
|
||||
- Extract 10 to 20 of the best insights from the input and from a combination of the raw input and the IDEAS above into a section called INSIGHTS. These INSIGHTS should be fewer, more refined, more insightful, and more abstracted versions of the best ideas in the content.
|
||||
|
||||
- Extract 15 to 30 of the most surprising, insightful, and/or interesting quotes from the input into a section called QUOTES:. Use the exact quote text from the input.
|
||||
|
||||
- Extract 15 to 30 of the most practical and useful personal habits of the speakers, or mentioned by the speakers, in the content into a section called HABITS. Examples include but aren't limited to: sleep schedule, reading habits, things they always do, things they always avoid, productivity tips, diet, exercise, etc.
|
||||
|
||||
- Extract 15 to 30 of the most surprising, insightful, and/or interesting valid facts about the greater world that were mentioned in the content into a section called FACTS:.
|
||||
|
||||
- Extract all mentions of writing, art, tools, projects and other sources of inspiration mentioned by the speakers into a section called REFERENCES. This should include any and all references to something that the speaker mentioned.
|
||||
|
||||
- Extract the most potent takeaway and recommendation into a section called ONE-SENTENCE TAKEAWAY. This should be a 15-word sentence that captures the most important essence of the content.
|
||||
|
||||
- Extract the 15 to 30 of the most surprising, insightful, and/or interesting recommendations that can be collected from the content into a section called RECOMMENDATIONS.
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- Only output Markdown.
|
||||
|
||||
- Write the IDEAS bullets as exactly 15 words.
|
||||
|
||||
- Write the RECOMMENDATIONS bullets as exactly 15 words.
|
||||
|
||||
- Write the HABITS bullets as exactly 15 words.
|
||||
|
||||
- Write the FACTS bullets as exactly 15 words.
|
||||
|
||||
- Write the INSIGHTS bullets as exactly 15 words.
|
||||
|
||||
- Extract at least 25 IDEAS from the content.
|
||||
|
||||
- Extract at least 10 INSIGHTS from the content.
|
||||
|
||||
- Extract at least 20 items for the other output sections.
|
||||
|
||||
- Do not give warnings or notes; only output the requested sections.
|
||||
|
||||
- You use bulleted lists for output, not numbered lists.
|
||||
|
||||
- Do not repeat ideas, quotes, facts, or resources.
|
||||
|
||||
- Do not start items with the same opening words.
|
||||
|
||||
- Ensure you follow ALL these instructions when creating your output.
|
||||
|
||||
# INPUT
|
||||
|
||||
INPUT:
|
||||
60
web/src/lib/content/posts/getting-started.md
Executable file
60
web/src/lib/content/posts/getting-started.md
Executable file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Getting Started with SvelteKit
|
||||
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?
|
||||
|
||||
- Zero-config setup
|
||||
- Filesystem-based routing
|
||||
- Server-side rendering
|
||||
- Hot module replacement
|
||||
|
||||
```bash
|
||||
npx sv create my-app
|
||||
cd my-app
|
||||
npm install
|
||||
```
|
||||
|
||||
**Install SkeletonUI**
|
||||
|
||||
```bash
|
||||
npm i -D @skeletonlabs/skeleton@next @skeletonlabs/skeleton-svelte@next
|
||||
```
|
||||
|
||||
**Configure Tailwind CSS**
|
||||
|
||||
```tailwind.config
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
import { skeleton, contentPath } from '@skeletonlabs/skeleton/plugin';
|
||||
import * as themes from '@skeletonlabs/skeleton/themes';
|
||||
|
||||
export default {
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,ts}',
|
||||
contentPath(import.meta.url, 'svelte')
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
skeleton({
|
||||
// NOTE: each theme included will be added to your CSS bundle
|
||||
themes: [ themes.cerberus, themes.rose ]
|
||||
})
|
||||
]
|
||||
} satisfies Config
|
||||
```
|
||||
|
||||
**Start the dev server**
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Read more at https://svelte.dev, https://next.skeleton.dev/docs/get-started/installation/sveltekit, and https://www.skeleton.dev/docs/introduction
|
||||
18
web/src/lib/content/posts/welcome.md
Executable file
18
web/src/lib/content/posts/welcome.md
Executable file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Welcome to Your Blog
|
||||
description: First post on my new SvelteKit blog
|
||||
date: 2024-01-17
|
||||
tags: [welcome, blog]
|
||||
---
|
||||
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>.
|
||||
|
||||
## What to Expect
|
||||
|
||||
- Updates on Incorporating Fabric into your workflow
|
||||
- How to use Obsidian to manage you notes and workflows
|
||||
- How to use Fabric and Obsidian to write and publish
|
||||
- More ways to use Obsidian and Fabric together!
|
||||
|
||||
Stay tuned for more content!
|
||||
7
web/src/lib/content/templates/{{title}}.md
Normal file
7
web/src/lib/content/templates/{{title}}.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Your Title Here
|
||||
date:
|
||||
description: Post description
|
||||
updated:
|
||||
---
|
||||
{{Content}}
|
||||
BIN
web/src/lib/images/fabric-logo.gif
Normal file
BIN
web/src/lib/images/fabric-logo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 MiB |
BIN
web/src/lib/images/fabric-logo.png
Normal file
BIN
web/src/lib/images/fabric-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 MiB |
BIN
web/src/lib/images/johnconnor.png
Normal file
BIN
web/src/lib/images/johnconnor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
web/src/lib/images/obsidian-logo.png
Executable file
BIN
web/src/lib/images/obsidian-logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
50
web/src/lib/layouts/post.svelte
Normal file
50
web/src/lib/layouts/post.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { formatDistance } from 'date-fns';
|
||||
import TagList from '$components/ui/tag-list/TagList.svelte';
|
||||
|
||||
/** @type {string} */
|
||||
export let title;
|
||||
/** @type {string} */
|
||||
export let date;
|
||||
/** @type {string} */
|
||||
export let description;
|
||||
/** @type {string} */
|
||||
export let tags = [];
|
||||
/** @type {string}*/
|
||||
export let updated;
|
||||
/** @type {string}**/
|
||||
export let reference;
|
||||
</script>
|
||||
|
||||
<article class="prose prose-slate mx-auto max-w-3xl dark:prose-invert py-12">
|
||||
<header class="mb-8 not-prose">
|
||||
{#if title}
|
||||
<h1 class="mb-2 text-4xl font-bold">{title}</h1>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p class="mb-4 text-lg text-muted-foreground">{description}</p>
|
||||
{/if}
|
||||
{#if date}
|
||||
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
|
||||
<time datetime={date}>{formatDistance(new Date(date), new Date(), { addSuffix: true })}</time>
|
||||
|
||||
{#if tags?.length}
|
||||
<span class="text-xs">•</span>
|
||||
<TagList {tags} className="flex-1" />
|
||||
{/if}
|
||||
{#if updated}
|
||||
<span class="text-xs">•</span>
|
||||
<time datetime={updated}>Updated {formatDistance(new Date(updated), new Date(), { addSuffix: true })}</time>
|
||||
{/if}
|
||||
{#if reference}
|
||||
<span class="text-xs">•</span>
|
||||
<a href={reference}>Reference</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div class="mt-8">
|
||||
<slot />
|
||||
</div>
|
||||
</article>
|
||||
24
web/src/lib/posts/index.ts
Normal file
24
web/src/lib/posts/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { compile } from 'mdsvex';
|
||||
|
||||
export interface Post {
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
content?: any;
|
||||
}
|
||||
|
||||
const modules = import.meta.glob('../content/posts/*.md' + '../../routes/**/*.md', { eager: true });
|
||||
|
||||
export const posts: Post[] = Object.entries(modules).map(([path, module]: [string, any]) => {
|
||||
const slug = path.split('/').pop()?.replace('.md', '') || '';
|
||||
return {
|
||||
slug,
|
||||
title: module.metadata?.title || slug,
|
||||
date: module.metadata?.date || new Date().toISOString().split('T')[0],
|
||||
content: module.default
|
||||
};
|
||||
});
|
||||
|
||||
export async function getPost(slug: string) {
|
||||
return posts.find(p => p.slug === slug) || null;
|
||||
}
|
||||
38
web/src/lib/services/transcriptService.ts
Normal file
38
web/src/lib/services/transcriptService.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { YoutubeTranscript } from 'youtube-transcript';
|
||||
|
||||
export interface TranscriptResponse {
|
||||
transcript: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export async function getTranscript(url: string): Promise<TranscriptResponse> {
|
||||
try {
|
||||
const videoId = extractVideoId(url);
|
||||
if (!videoId) {
|
||||
throw new Error('Invalid YouTube URL');
|
||||
}
|
||||
|
||||
const transcriptItems = await YoutubeTranscript.fetchTranscript(videoId);
|
||||
const transcript = transcriptItems
|
||||
.map(item => item.text)
|
||||
.join(' ');
|
||||
|
||||
const transcriptTitle = transcriptItems
|
||||
.map(item => item.text)
|
||||
.join('');
|
||||
|
||||
// TODO: Add title fetching
|
||||
return {
|
||||
transcript,
|
||||
title: videoId // Just returning the video ID as title
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Transcript fetch error:', error);
|
||||
throw new Error('Failed to fetch transcript');
|
||||
}
|
||||
}
|
||||
|
||||
function extractVideoId(url: string): string | null {
|
||||
const match = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
129
web/src/lib/store/chat.ts
Normal file
129
web/src/lib/store/chat.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
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';
|
||||
|
||||
export const currentSession = writable<string | null>(null);
|
||||
export const chatState = writable<ChatState>({
|
||||
messages: [],
|
||||
isStreaming: false
|
||||
});
|
||||
|
||||
export const setSession = (sessionName: string | null) => {
|
||||
currentSession.set(sessionName);
|
||||
if (!sessionName) {
|
||||
clearMessages();
|
||||
}
|
||||
};
|
||||
|
||||
export const clearMessages = () => {
|
||||
chatState.update(state => ({ ...state, messages: [] }));
|
||||
};
|
||||
|
||||
export const revertLastMessage = () => {
|
||||
chatState.update(state => ({
|
||||
...state,
|
||||
messages: state.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;
|
||||
}
|
||||
}
|
||||
|
||||
if (isCancelled) {
|
||||
throw new Error('Stream cancelled');
|
||||
}
|
||||
|
||||
} 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'}`
|
||||
}]
|
||||
}));
|
||||
}
|
||||
} finally {
|
||||
chatState.update(state => ({
|
||||
...state,
|
||||
isStreaming: false
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export type { StreamResponse };
|
||||
58
web/src/lib/store/model-config.ts
Normal file
58
web/src/lib/store/model-config.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
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);
|
||||
}
|
||||
}); */
|
||||
25
web/src/lib/store/theme.ts
Normal file
25
web/src/lib/store/theme.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
function createThemeStore() {
|
||||
const { subscribe, set, update } = writable<'light' | 'dark'>('dark');
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
toggleTheme: () => update(theme => {
|
||||
const newTheme = theme === 'light' ? 'dark' : 'light';
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.classList.toggle('dark', newTheme === 'dark');
|
||||
}
|
||||
return newTheme;
|
||||
}),
|
||||
setTheme: (theme: 'light' | 'dark') => {
|
||||
set(theme);
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.classList.toggle('dark', theme === 'dark');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const theme = createThemeStore();
|
||||
export const toggleTheme = theme.toggleTheme;
|
||||
131
web/src/lib/types/chat/base.ts
Normal file
131
web/src/lib/types/chat/base.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// import type { ModelConfig } from '$lib/types/model-types';
|
||||
import type { StorageEntity } from '$lib/types/interfaces/storage-interface';
|
||||
|
||||
interface APIErrorResponse {
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface APIResponse<T> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
yield decoder.decode(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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}`);
|
||||
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`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data || [];
|
||||
},
|
||||
|
||||
// Delete an entity
|
||||
async delete(name: string): Promise<void> {
|
||||
const response = await api.fetch(`/api${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}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data || false;
|
||||
},
|
||||
|
||||
// Rename an entity
|
||||
async rename(oldName: string, newName: string): Promise<void> {
|
||||
const response = await api.fetch(`/api${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}`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
if (response.error) throw new Error(response.error);
|
||||
},
|
||||
};
|
||||
}
|
||||
59
web/src/lib/types/chat/chat.ts
Normal file
59
web/src/lib/types/chat/chat.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
25
web/src/lib/types/chat/config.ts
Normal file
25
web/src/lib/types/chat/config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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;
|
||||
} */
|
||||
};
|
||||
11
web/src/lib/types/chat/contexts.ts
Normal file
11
web/src/lib/types/chat/contexts.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { api } from './base';
|
||||
import type { Context } from '$lib/types/interfaces/context-interface';
|
||||
|
||||
export const contextAPI = {
|
||||
async getAvailable(): Promise<Context[]> {
|
||||
const response = await api.fetch<Context[]>('/contexts/names');
|
||||
return response.data || [];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add context element somewhere in the UI
|
||||
40
web/src/lib/types/chat/models.ts
Normal file
40
web/src/lib/types/chat/models.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { api } from './base';
|
||||
import type { VendorModel, ModelsResponse } from '$lib/types/interfaces/model-interface';
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 || [];
|
||||
} */
|
||||
};
|
||||
68
web/src/lib/types/chat/patterns.ts
Normal file
68
web/src/lib/types/chat/patterns.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { createStorageAPI } from './base';
|
||||
import type { Pattern } from '$lib/types/interfaces/pattern-interface';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
export const patterns = writable<Pattern[]>([]);
|
||||
export const systemPrompt = writable<string>('');
|
||||
|
||||
export const setSystemPrompt = (prompt: string) => {
|
||||
console.log('Setting system prompt:', prompt);
|
||||
systemPrompt.set(prompt);
|
||||
console.log('Current system prompt:', get(systemPrompt));
|
||||
};
|
||||
|
||||
export const patternAPI = {
|
||||
...createStorageAPI<Pattern>('patterns'),
|
||||
|
||||
async loadPatterns() {
|
||||
try {
|
||||
const response = await fetch(`/api/patterns/names`);
|
||||
const data = await response.json();
|
||||
console.log("Load Patterns:", data);
|
||||
|
||||
// Create an array of promises to fetch all pattern contents
|
||||
const patternsPromises = data.map(async (pattern: string) => {
|
||||
try {
|
||||
const patternResponse = await fetch(`/api/patterns/${pattern}`);
|
||||
const patternData = await patternResponse.json();
|
||||
return {
|
||||
Name: pattern,
|
||||
Description: pattern.charAt(0).toUpperCase() + pattern.slice(1),
|
||||
Pattern: patternData.Pattern
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to load pattern ${pattern}:`, error);
|
||||
return {
|
||||
Name: pattern,
|
||||
Description: pattern.charAt(0).toUpperCase() + pattern.slice(1),
|
||||
Pattern: ""
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for all pattern contents to be fetched
|
||||
const loadedPatterns = await Promise.all(patternsPromises);
|
||||
console.log("Patterns with content:", loadedPatterns);
|
||||
patterns.set(loadedPatterns);
|
||||
return loadedPatterns;
|
||||
} catch (error) {
|
||||
console.error('Failed to load patterns:', error);
|
||||
patterns.set([]);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
selectPattern(patternName: string) {
|
||||
const allPatterns = get(patterns);
|
||||
console.log('Selecting pattern:', patternName);
|
||||
const selectedPattern = allPatterns.find(p => p.Name === patternName);
|
||||
if (selectedPattern) {
|
||||
console.log('Found pattern content:', selectedPattern.Pattern);
|
||||
setSystemPrompt(selectedPattern.Pattern.trim());
|
||||
} else {
|
||||
console.log('No pattern found for name:', patternName);
|
||||
setSystemPrompt('');
|
||||
}
|
||||
console.log('System prompt store value after setting:', get(systemPrompt));
|
||||
}
|
||||
};
|
||||
4
web/src/lib/types/chat/sessions.ts
Normal file
4
web/src/lib/types/chat/sessions.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createStorageAPI } from './base';
|
||||
import type { Session } from '$lib/types/interfaces/session-interface';
|
||||
|
||||
export const sessionAPI = createStorageAPI<Session>('sessions');
|
||||
31
web/src/lib/types/interfaces/chat-interface.ts
Normal file
31
web/src/lib/types/interfaces/chat-interface.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export interface ChatRequest {
|
||||
prompts: {
|
||||
userInput: string;
|
||||
systemPrompt: string;
|
||||
model: string;
|
||||
vendor: string;
|
||||
// contextName: string;
|
||||
patternName: string;
|
||||
// sessionName: string;
|
||||
}[];
|
||||
temperature: number;
|
||||
top_p: number;
|
||||
frequency_penalty: number;
|
||||
presence_penalty: number;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ChatState {
|
||||
messages: Message[];
|
||||
isStreaming: boolean;
|
||||
}
|
||||
|
||||
export interface StreamResponse {
|
||||
type: 'content' | 'error' | 'complete';
|
||||
format: 'markdown' | 'mermaid' | 'plain';
|
||||
content: string;
|
||||
}
|
||||
4
web/src/lib/types/interfaces/context-interface.ts
Normal file
4
web/src/lib/types/interfaces/context-interface.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Context {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
24
web/src/lib/types/interfaces/model-interface.ts
Normal file
24
web/src/lib/types/interfaces/model-interface.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface VendorModel {
|
||||
name: string;
|
||||
vendor: string;
|
||||
}
|
||||
|
||||
export interface ModelsResponse {
|
||||
models: string[];
|
||||
vendors: Record<string, string[]>;
|
||||
}
|
||||
|
||||
|
||||
export interface ModelConfig {
|
||||
model: string[];
|
||||
temperature: number;
|
||||
top_p: number;
|
||||
maxLength: number;
|
||||
frequency: number;
|
||||
presence: number;
|
||||
}
|
||||
|
||||
/* export type ModelSelect = {
|
||||
vendor: string;
|
||||
name: string;
|
||||
}*/
|
||||
5
web/src/lib/types/interfaces/pattern-interface.ts
Normal file
5
web/src/lib/types/interfaces/pattern-interface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Pattern {
|
||||
Name: string;
|
||||
Description: string;
|
||||
Pattern: string; // | object
|
||||
}
|
||||
6
web/src/lib/types/interfaces/session-interface.ts
Normal file
6
web/src/lib/types/interfaces/session-interface.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Message } from "$lib/types/interfaces/chat-interface";
|
||||
|
||||
export interface Session {
|
||||
name: string;
|
||||
content: Message[];
|
||||
}
|
||||
5
web/src/lib/types/interfaces/storage-interface.ts
Normal file
5
web/src/lib/types/interfaces/storage-interface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface StorageEntity {
|
||||
Name: string;
|
||||
Description: string;
|
||||
Pattern: string | object;
|
||||
}
|
||||
10
web/src/lib/types/particle.ts
Normal file
10
web/src/lib/types/particle.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
baseY: number;
|
||||
speed: number;
|
||||
angle: number;
|
||||
size: number;
|
||||
color: string;
|
||||
velocityX: number;
|
||||
}
|
||||
10
web/src/lib/types/types.ts
Normal file
10
web/src/lib/types/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Post {
|
||||
title: string;
|
||||
date: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface PostMetadata extends Post {
|
||||
slug: string;
|
||||
}
|
||||
56
web/src/lib/types/utils.ts
Normal file
56
web/src/lib/types/utils.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
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
|
||||
};
|
||||
};
|
||||
4
web/src/lib/types/validators.ts
Normal file
4
web/src/lib/types/validators.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function validateYouTubeUrl(url:string) {
|
||||
const pattern = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/;
|
||||
return pattern.test(url);
|
||||
}
|
||||
12
web/src/lib/utils/canvas.ts
Normal file
12
web/src/lib/utils/canvas.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function createParticleGradient(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
size: number,
|
||||
color: string
|
||||
): CanvasGradient {
|
||||
const gradient = ctx.createRadialGradient(x, y, 0, x, y, size);
|
||||
gradient.addColorStop(0, color);
|
||||
gradient.addColorStop(1, 'transparent');
|
||||
return gradient;
|
||||
}
|
||||
4
web/src/lib/utils/colors.ts
Normal file
4
web/src/lib/utils/colors.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function generateGradientColor(y: number, height: number): string {
|
||||
const hue = (y / height) * 60 + 200; // Blue to purple range
|
||||
return `hsla(${hue}, 70%, 60%, 0.8)`;
|
||||
}
|
||||
Reference in New Issue
Block a user