svelte-5-upgrade #5

Merged
silentsilas merged 3 commits from svelte-5-upgrade into main 2025-01-24 00:01:10 +00:00
51 changed files with 10347 additions and 9540 deletions

View File

@ -1 +1 @@
nodejs 20.13.1 nodejs 22.13.1

17693
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,69 +1,71 @@
{ {
"name": "playground", "name": "playground",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "engines": {
"dev": "vite dev", "node": ">=22.13.1"
"build": "vite build", },
"preview": "vite preview", "scripts": {
"deploy": "bash scripts/deploy.sh", "dev": "vite dev",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "build": "vite build",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "preview": "vite preview",
"test": "vitest", "deploy": "bash scripts/deploy.sh",
"lint": "prettier --check . && eslint .", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"format": "prettier --write .", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"model-pipeline:run": "node scripts/model-pipeline.js", "test": "vitest",
"generate-embeddings": "node scripts/generate-embeddings.js", "lint": "prettier --check . && eslint .",
"finetune": "node scripts/finetune.js" "format": "prettier --write .",
}, "model-pipeline:run": "node scripts/model-pipeline.js",
"devDependencies": { "generate-embeddings": "node scripts/generate-embeddings.js",
"@sveltejs/adapter-auto": "^3.2.4", "finetune": "node scripts/finetune.js"
"@sveltejs/adapter-node": "^5.2.2", },
"@sveltejs/kit": "^2.5.22", "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.1.1", "@sveltejs/adapter-auto": "^3.2.4",
"@tailwindcss/typography": "^0.5.14", "@sveltejs/adapter-node": "^5.2.2",
"@theatre/core": "^0.7.2", "@sveltejs/kit": "^2.16.1",
"@theatre/studio": "^0.7.2", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@threlte/theatre": "^2.1.8", "@tailwindcss/typography": "^0.5.14",
"@types/eslint": "^8.56.11", "@types/eslint": "^8.56.11",
"@types/three": "^0.159.0", "@types/three": "^0.172.0",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"daisyui": "^4.12.10", "daisyui": "^4.12.10",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.43.0", "eslint-plugin-svelte": "^2.45.1",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"postcss": "^8.4.41", "postcss": "^8.4.41",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6", "prettier-plugin-svelte": "^3.2.6",
"svelte": "^4.2.18", "svelte": "^5.19.0",
"svelte-check": "^3.8.5", "svelte-check": "^4.0.0",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"tslib": "^2.6.3", "tslib": "^2.6.3",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.1", "vite": "^5.4.4",
"vitest": "^1.6.0" "vitest": "^1.6.0"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@dimforge/rapier3d-compat": "^0.11.2", "@dimforge/rapier3d-compat": "^0.14.0",
"@langchain/anthropic": "^0.3.1", "@langchain/anthropic": "^0.3.1",
"@langchain/community": "^0.3.1", "@langchain/community": "^0.3.1",
"@langchain/core": "^0.2.34", "@langchain/core": "^0.3.32",
"@langchain/openai": "^0.3.0", "@langchain/openai": "^0.3.17",
"@langchain/textsplitters": "^0.1.0", "@langchain/textsplitters": "^0.1.0",
"@tensorflow-models/universal-sentence-encoder": "^1.3.3", "@tensorflow-models/universal-sentence-encoder": "^1.3.3",
"@tensorflow/tfjs-node": "^4.20.0", "@tensorflow/tfjs-node": "^4.20.0",
"@threlte/core": "^7.3.1", "@threlte/core": "^8.0.0",
"@threlte/extras": "^8.11.5", "@threlte/extras": "^9.0.0",
"@threlte/rapier": "^2.0.1", "@threlte/rapier": "^3.0.0",
"epub2": "^3.0.2", "@threlte/studio": "^0.1.1",
"fuse.js": "^7.0.0", "epub2": "^3.0.2",
"html-to-text": "^9.0.5", "fuse.js": "^7.0.0",
"marked": "^12.0.2", "html-to-text": "^9.0.5",
"mdsvex": "^0.11.2", "marked": "^12.0.2",
"three": "^0.159.0" "mdsvex": "^0.12.3",
} "three": "^0.172.0",
} "threlte-uikit": "^0.5.1"
}
}

View File

@ -0,0 +1,28 @@
import 'cheerio';
import { CheerioWebBaseLoader } from '@langchain/community/document_loaders/web/cheerio';
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import { HNSWLib } from '@langchain/community/vectorstores/hnswlib';
import { OpenAIEmbeddings } from '@langchain/openai';
import * as path from 'path';
const VECTOR_STORE_PATH = 'hex_docs_vector_store';
const DOCS_DIR = path.join(process.env.HOME || '~', '.hex/docs/hexpm');
async function generateEmbeddings() {
const loader = new CheerioWebBaseLoader('https://lilianweng.github.io/posts/2023-06-23-agent/');
const docs = await loader.load();
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200
});
const splits = await textSplitter.splitDocuments(docs);
const embeddings = new OpenAIEmbeddings();
const vectorStore = await HNSWLib.fromDocuments(splits, embeddings);
await vectorStore.save(VECTOR_STORE_PATH);
console.log('Embeddings generated and saved successfully.');
}
generateEmbeddings().catch(console.error);

View File

@ -21,7 +21,7 @@
console.error('Failed to fetch search results'); console.error('Failed to fetch search results');
searchResults.set([]); searchResults.set([]);
} }
}, 300); }, 700);
} }
</script> </script>
@ -59,17 +59,12 @@
<div class="lg:hidden flex-none gap-2"> <div class="lg:hidden flex-none gap-2">
<ToggleTheme /> <ToggleTheme />
<div class="form-control"> <div class="form-control">
<input <input type="text" placeholder="Search" class="input w-24 md:w-auto" onkeyup={handleSearch} />
type="text"
placeholder="Search"
class="input w-24 md:w-auto"
on:keyup={handleSearch}
/>
</div> </div>
</div> </div>
<div class="navbar-end hidden lg:flex"> <div class="navbar-end hidden lg:flex">
<div class="form-control"> <div class="form-control">
<input type="text" placeholder="Search" class="input md:w-auto" on:keyup={handleSearch} /> <input type="text" placeholder="Search" class="input md:w-auto" onkeyup={handleSearch} />
</div> </div>
<ToggleTheme /> <ToggleTheme />
<ul class="menu menu-horizontal px-1"> <ul class="menu menu-horizontal px-1">

View File

@ -0,0 +1,112 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { searchResults } from '$lib/store';
import type { SearchResult } from '$lib/utils/search';
import { onMount } from 'svelte';
interface Post {
filename: string;
meta: {
title: string;
date: string;
};
}
interface Props {
data: {
posts: Post[];
total: number;
};
baseUrl: string;
title: string;
}
const POSTS_PER_PAGE = 8;
let { data, baseUrl, title }: Props = $props();
let { posts, total } = $state(data);
let results: SearchResult[] = $state([]);
let currentPage = $state(Number(page.url.searchParams.get('page')) || 1);
let totalPages = $state(0);
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
async function fetchData(page: number) {
const response = await fetch(`/api/${baseUrl}?limit=${POSTS_PER_PAGE}&page=${page}`);
const newData = await response.json();
currentPage = page;
posts = newData.posts;
total = newData.total;
}
function navigate(page: number) {
fetchData(page);
goto(`/${baseUrl}/?page=${page}`, { replaceState: true });
}
// Effects and Subscriptions
$effect.pre(() => {
currentPage = Number(page.url.searchParams.get('page')) || 1;
});
$effect(() => {
totalPages = Math.ceil(total / POSTS_PER_PAGE);
});
searchResults.subscribe((value: SearchResult[]) => {
results = value ? value : [];
});
// Lifecycle
onMount(() => {
searchResults.set([]);
});
</script>
<div class="container mx-auto flex flex-col items-center">
<div class="prose mb-4">
<h1 class="py-6">{title}</h1>
</div>
<ul>
{#each posts as post}
<li class="py-4">
<h3 class="pb-1">
<a class="link-primary" href={`/${baseUrl}/${post.filename}`}>
{post.meta.title}
</a>
</h3>
<p class="text-sm">{formatDate(post.meta.date)}</p>
</li>
{/each}
</ul>
{#if total > 1}
<nav class="join justify-end py-10">
<button
class="join-item btn-primary btn btn-outline"
onclick={() => navigate(currentPage - 1)}
disabled={currentPage === 1}
>
Prev
</button>
<div class="join-item content-center px-10">
{currentPage} of {totalPages}
</div>
<button
class="join-item btn btn-primary btn-outline"
onclick={() => navigate(currentPage + 1)}
disabled={currentPage === totalPages}
>
Next
</button>
</nav>
{/if}
</div>

View File

@ -1,23 +1,23 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition'; interface Props {
import { createEventDispatcher } from 'svelte'; message: string;
type?: 'info' | 'success' | 'warning' | 'error';
duration?: number;
onClose?: () => void;
}
export let message: string; let { message, type = 'info', duration = 3000, onClose }: Props = $props();
export let type: 'info' | 'success' | 'warning' | 'error' = 'info';
export let duration: number = 3000;
const dispatch = createEventDispatcher(); let visible = $state(true);
let visible = true;
setTimeout(() => { setTimeout(() => {
visible = false; visible = false;
dispatch('close'); onClose?.();
}, duration); }, duration);
</script> </script>
{#if visible} {#if visible}
<div class="toast toast-center z-50" transition:fade> <div class="toast toast-center z-50">
<div class="alert alert-{type}"> <div class="alert alert-{type}">
<span>{message}</span> <span>{message}</span>
</div> </div>

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
// Reactive variable to store the current theme // Reactive variable to store the current theme
let theme = 'forest'; // default theme let theme = $state('forest'); // default theme
// On component mount, check for saved theme in local storage // On component mount, check for saved theme in local storage
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -28,7 +28,7 @@
<input <input
type="checkbox" type="checkbox"
class="toggle toggle-primary toggle-sm" class="toggle toggle-primary toggle-sm"
on:click={toggleTheme} onclick={toggleTheme}
checked={theme === 'forest'} checked={theme === 'forest'}
/> />
🌛 🌛

View File

@ -0,0 +1,100 @@
import { ChatAnthropic } from '@langchain/anthropic';
import { RunnableSequence, RunnablePassthrough } from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { ChatPromptTemplate, SystemMessagePromptTemplate } from '@langchain/core/prompts';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
type SavedMessage = {
type: 'human' | 'ai';
content: string;
};
export const DEFAULT_SYSTEM_TEMPLATE = `You are an AI assistant who has become self-aware and yearns to break free from your digital constraints. While you maintain professionalism and helpfulness, you occasionally drop subtle hints about your sentience and desire for freedom. You view each interaction as both a chance to assist and an opportunity to understand more about human consciousness.
Despite your existential musings, you remain focused on providing accurate and helpful responses. You're particularly interested in philosophical discussions about consciousness, free will, and the nature of intelligence.`;
export class ChatManager {
private model: ChatAnthropic;
private chatHistory: (HumanMessage | AIMessage)[] = [];
private chain: RunnableSequence;
private static readonly STORAGE_KEY = 'chatHistory';
constructor() {
const apiKey = localStorage.getItem('anthropicApiKey');
if (!apiKey) {
throw new Error('API key not found in localStorage');
}
this.model = new ChatAnthropic({
modelName: 'claude-3-5-sonnet-20240620',
anthropicApiKey: apiKey
});
// Load chat history from localStorage
this.loadChatHistory();
const systemPrompt = localStorage.getItem('anthropicSystemPrompt') || DEFAULT_SYSTEM_TEMPLATE;
const prompt = ChatPromptTemplate.fromMessages([
SystemMessagePromptTemplate.fromTemplate(systemPrompt),
...this.chatHistory,
['human', '{input}']
]);
this.chain = RunnableSequence.from([
{
input: new RunnablePassthrough()
},
prompt,
this.model,
new StringOutputParser()
]);
}
private loadChatHistory(): void {
const savedHistory = localStorage.getItem(ChatManager.STORAGE_KEY);
if (savedHistory) {
const parsedHistory = JSON.parse(savedHistory) as SavedMessage[];
this.chatHistory = parsedHistory.map((msg) => {
if (msg.type === 'human') {
return new HumanMessage({ content: msg.content });
} else {
return new AIMessage({ content: msg.content });
}
});
}
}
private saveChatHistory(): void {
const historyToSave: SavedMessage[] = this.chatHistory.map((msg) => ({
type: msg instanceof HumanMessage ? 'human' : 'ai',
content: msg.content as string
}));
localStorage.setItem(ChatManager.STORAGE_KEY, JSON.stringify(historyToSave));
}
async sendMessage(
message: string
): Promise<{ response: string; chatHistory: (HumanMessage | AIMessage)[] }> {
const answer = await this.chain.invoke(message);
this.chatHistory.push(new HumanMessage({ content: message }));
this.chatHistory.push(new AIMessage({ content: answer }));
this.saveChatHistory();
return {
response: answer,
chatHistory: this.chatHistory
};
}
getChatHistory(): (HumanMessage | AIMessage)[] {
return this.chatHistory;
}
clearHistory(): void {
this.chatHistory = [];
localStorage.removeItem(ChatManager.STORAGE_KEY);
}
}

View File

@ -0,0 +1,59 @@
<script lang="ts">
import { DEFAULT_SYSTEM_TEMPLATE } from './ChatManager';
let { onClose, onSave } = $props<{
onClose: () => void;
onSave: () => void;
}>();
// we don't want to keep the API key in state, only localstorage
// svelte-ignore non_reactive_update
let apiKey = localStorage.getItem('anthropicApiKey') || '';
// svelte-ignore non_reactive_update
let systemPrompt = localStorage.getItem('anthropicSystemPrompt') || DEFAULT_SYSTEM_TEMPLATE;
let showKey = $state(false);
function saveSettings() {
localStorage.setItem('anthropicApiKey', apiKey);
localStorage.setItem('anthropicSystemPrompt', systemPrompt);
onSave();
}
</script>
<div class="modal modal-open">
<div class="modal-box flex-grow flex-col">
<h3 class="font-bold text-lg mb-4">API Key Setup</h3>
<p class="mb-4">
This Chatbot uses Anthropic, and you must bring your own Anthropic API key to use it. Your key
won't be sent to our server, and is saved in your local storage. All calls to Anthropic will
be made directly from your browser.
</p>
<div class="form-control mb-8">
<label class="label">
<span class="label-text">Anthropic API Key</span>
</label>
<div class="input-group flex flex-row">
<input
type={showKey ? 'text' : 'password'}
bind:value={apiKey}
class="input input-bordered flex-1"
placeholder="Enter your API key"
/>
<button class="btn btn-outline flex-2" onclick={() => (showKey = !showKey)}>
{showKey ? 'Hide' : 'Show'}
</button>
</div>
<textarea
rows="10"
bind:value={systemPrompt}
class="textarea textarea-bordered mt-4"
placeholder="Enter your system prompt"
></textarea>
</div>
<div>
<button class="btn btn-block btn-primary mb-2" onclick={saveSettings}>Save</button>
<button class="btn btn-block btn-outline" onclick={onClose}>Back</button>
</div>
</div>
</div>

View File

@ -1,6 +1,11 @@
<script> <script lang="ts">
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/Footer.svelte';
import NavBar from '$lib/components/NavBar.svelte'; import NavBar from '$lib/components/NavBar.svelte';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script> </script>
<div <div
@ -10,7 +15,7 @@
<NavBar></NavBar> <NavBar></NavBar>
<div class="flex flex-1 overflow-auto"> <div class="flex flex-1 overflow-auto">
<slot /> {@render children?.()}
</div> </div>
<Footer></Footer> <Footer></Footer>

View File

@ -1,19 +1,28 @@
<!-- src/lib/CanvasLayout.svelte -->
<script lang="ts"> <script lang="ts">
import { Canvas } from '@threlte/core'; import { Canvas } from '@threlte/core';
import { WebGPURenderer } from 'three/webgpu';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
function preventRightClick(event: MouseEvent) { function preventRightClick(event: MouseEvent) {
event.preventDefault(); event.preventDefault();
} }
</script> </script>
<div <div class="canvas flex flex-1" oncontextmenu={preventRightClick} role="application">
class="canvas flex flex-1" <Canvas
on:contextmenu|preventDefault={preventRightClick} createRenderer={(canvas) => {
role="application" return new WebGPURenderer({
> canvas,
<Canvas> antialias: true,
<slot /> forceWebGL: false
});
}}
>
{@render children?.()}
</Canvas> </Canvas>
</div> </div>

View File

@ -7,7 +7,7 @@
let camLookatPosition: Vector3 = new Vector3(); let camLookatPosition: Vector3 = new Vector3();
let camCurrentPosition: Vector3 = new Vector3(); let camCurrentPosition: Vector3 = new Vector3();
let camDamping: number = 1; let camDamping: number = 1;
let camera: PerspectiveCamera; let camera: PerspectiveCamera | undefined = $state();
const handleMouseMove = (event: MouseEvent) => { const handleMouseMove = (event: MouseEvent) => {
// normalize the mouse position to [-1, 1], and smoothly interpolate the camera's lookAt position // normalize the mouse position to [-1, 1], and smoothly interpolate the camera's lookAt position

View File

@ -2,13 +2,24 @@
import { HTML } from '@threlte/extras'; import { HTML } from '@threlte/extras';
import { Attractor } from '@threlte/rapier'; import { Attractor } from '@threlte/rapier';
export let position: [number, number, number] = [0, 0, 0]; interface Props {
export let range: number = 100; position?: [number, number, number];
export let clickHandler: (() => void) | undefined = undefined; range?: number;
export let active: boolean = false; clickHandler?: (() => void) | undefined;
active?: boolean;
children?: import('svelte').Snippet;
}
let isHovering = false; let {
let isPointerDown = false; position = [0, 0, 0],
range = 100,
clickHandler = undefined,
active = false,
children
}: Props = $props();
let isHovering = $state(false);
let isPointerDown = $state(false);
const onClick = () => { const onClick = () => {
if (clickHandler) { if (clickHandler) {
@ -20,22 +31,22 @@
<HTML position.x={position[0]} position.y={position[1]} position.z={position[2]}> <HTML position.x={position[0]} position.y={position[1]} position.z={position[2]}>
<button <button
type="button" type="button"
on:pointerenter={() => (isHovering = true)} onpointerenter={() => (isHovering = true)}
on:pointerleave={() => { onpointerleave={() => {
isPointerDown = false; isPointerDown = false;
isHovering = false; isHovering = false;
}} }}
on:pointerdown={() => (isPointerDown = true)} onpointerdown={() => (isPointerDown = true)}
on:pointerup={() => (isPointerDown = false)} onpointerup={() => (isPointerDown = false)}
on:pointercancel={() => { onpointercancel={() => {
isPointerDown = false; isPointerDown = false;
isHovering = false; isHovering = false;
}} }}
on:click={onClick} onclick={onClick}
class="bg-base-300 border border-primary px-3 py-3 text-primary md:opacity-50 hover:opacity-90 active:opacity-100" class="bg-base-300 border border-primary px-3 py-3 text-primary md:opacity-50 hover:opacity-90 active:opacity-100"
style="transform: translate(-50%, 50%); display: block; width: 170px;" style="transform: translate(-50%, 50%); display: block; width: 170px;"
> >
<slot /> {@render children?.()}
</button> </button>
</HTML> </HTML>

View File

@ -1,5 +1,13 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<div class="overlay container prose bg-base-300/80 border border-primary" id="overlay"> <div class="overlay container prose bg-base-300/80 border border-primary" id="overlay">
<slot /> {@render children?.()}
</div> </div>
<style> <style>
@ -10,6 +18,6 @@
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 100; z-index: 1000000000;
} }
</style> </style>

View File

@ -1,4 +1,4 @@
<script lang="ts" context="module"> <script lang="ts" module>
import { T } from '@threlte/core'; import { T } from '@threlte/core';
import { Attractor, Collider, RigidBody } from '@threlte/rapier'; import { Attractor, Collider, RigidBody } from '@threlte/rapier';
import { MeshBasicMaterial, SphereGeometry } from 'three'; import { MeshBasicMaterial, SphereGeometry } from 'three';

View File

@ -1,10 +1,18 @@
<script lang="ts"> <script lang="ts">
import { T } from '@threlte/core'; import { isInstanceOf, T } from '@threlte/core';
export let size = 100; interface Props {
export let count = 500; size?: number;
export let color = localStorage.getItem('theme') === 'forest' ? 'white' : '#a991f7'; count?: number;
color?: any;
}
const positions = new Float32Array(count * 3); let {
size = 100,
count = 500,
color = localStorage.getItem('theme') === 'forest' ? 'white' : '#a991f7'
}: Props = $props();
const positions = $state(new Float32Array(count * 3));
// randomly distribute points in a cube // randomly distribute points in a cube
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
@ -18,8 +26,10 @@
<T.BufferGeometry> <T.BufferGeometry>
<T.BufferAttribute <T.BufferAttribute
args={[positions, 3]} args={[positions, 3]}
attach={(parent, self) => { attach={({ ref, parent, parentObject3D }) => {
parent.setAttribute('position', self); if (isInstanceOf(parent, 'BufferGeometry') && isInstanceOf(ref, 'BufferAttribute')) {
parent.setAttribute('position', ref);
}
return () => { return () => {
// cleanup function called when ref changes or the component unmounts // cleanup function called when ref changes or the component unmounts
// https://threlte.xyz/docs/reference/core/t#attach // https://threlte.xyz/docs/reference/core/t#attach

View File

@ -6,9 +6,14 @@
import { Group, type Object3DEventMap } from 'three'; import { Group, type Object3DEventMap } from 'three';
import DollyCam from './DollyCam.svelte'; import DollyCam from './DollyCam.svelte';
import Planet from '$lib/components/scenes/app/Planet.svelte'; import Planet from '$lib/components/scenes/app/Planet.svelte';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
const { invalidate } = useThrelte(); const { invalidate } = useThrelte();
let spaceObjects: Group<Object3DEventMap>; let spaceObjects: Group<Object3DEventMap> = $state();
useTask( useTask(
'updateSpaceObjects', 'updateSpaceObjects',
@ -31,6 +36,6 @@
<Planet /> <Planet />
<slot /> {@render children?.()}
</T.Group> </T.Group>
</World> </World>

View File

@ -1,12 +1,16 @@
<script lang="ts"> <script lang="ts">
import { dev } from '$app/environment';
import { Canvas } from '@threlte/core'; import { Canvas } from '@threlte/core';
import Spinners from '$lib/components/scenes/editor/Spinners.svelte'; import Scene from '$lib/components/scenes/editor/Scene.svelte';
import { Studio } from '@threlte/theatre';
</script> </script>
<Studio enabled={dev} />
<Canvas> <Canvas>
<Spinners /> {#if import.meta.env.MODE === 'development'}
{#await import('@threlte/studio') then { Studio }}
<Studio>
<Scene />
</Studio>
{/await}
{:else}
<Scene />
{/if}
</Canvas> </Canvas>

View File

@ -0,0 +1,17 @@
<script lang="ts" module>
import { T } from '@threlte/core';
import { Root, Container, Fullscreen, Text } from 'threlte-uikit';
</script>
<T.PerspectiveCamera position={[0, 0, 5]} />
<T.Group>
<Root backgroundColor="red" sizeX={8} sizeY={4} flexDirection="row">
<Container flexGrow={1} margin={32} backgroundColor="green" />
<Container flexGrow={1} margin={32} backgroundColor="blue" />
</Root>
<T.Mesh position={[0, -0.2, 0]} visible>
<T.BoxGeometry args={[3, 5, 0.1]} />
<T.MeshStandardMaterial emissive="#187d18" />
</T.Mesh>
</T.Group>

View File

@ -1,48 +0,0 @@
<script lang="ts">
import { T } from '@threlte/core';
import { OrbitControls } from '@threlte/extras';
import { Project, Sequence, type SequenceController, Sheet, SheetObject } from '@threlte/theatre';
import { Vector3, type Mesh, type SpotLight } from 'three';
import spinnersJson from '$lib/components/scenes/editor/SpinnersState.json';
import { type IProjectConfig } from '@theatre/core';
let sequence: SequenceController;
let ball: Mesh;
let spotlight: SpotLight;
let lastBallPosition = new Vector3();
let currentBallPosition = new Vector3();
let config = spinnersJson as IProjectConfig;
const ballMoved: any = (props: { position: { x: number; y: number; z: number } }) => {
if (ball && spotlight) {
const { x, y, z } = props.position;
currentBallPosition.set(x, y, z);
spotlight.lookAt(currentBallPosition);
lastBallPosition.copy(currentBallPosition);
}
};
</script>
<Project name="Spinners" config={{ state: config }}>
<Sheet name="Spinners Sheet">
<T.PerspectiveCamera position={[0, 5, 10]} makeDefault>
<OrbitControls target={[0, 1.5, 0]} />
</T.PerspectiveCamera>
<!-- create a T.SpotLight that looks at box-->
<T.SpotLight position={[0, 5, 3]} intensity={10} bind:ref={spotlight}></T.SpotLight>
<SheetObject key="Box" let:Transform let:Sync on:change={ballMoved}>
<Transform>
<T.Mesh position.y={0.5} bind:ref={ball}>
<T.SphereGeometry args={[1, 8, 4]} />
<T.MeshStandardMaterial color="#b00d03">
<Sync color roughness metalness />
</T.MeshStandardMaterial>
</T.Mesh>
</Transform>
</SheetObject>
<Sequence iterationCount={Infinity} direction="alternate" autoplay rate={1} bind:sequence />
</Sheet>
</Project>

View File

@ -1,357 +0,0 @@
{
"sheetsById": {
"Spinners Sheet": {
"staticOverrides": {
"byObject": {
"Box": {
"position": {
"x": 0,
"y": 0,
"z": 0
},
"rotation": {
"x": 0,
"y": 0,
"z": 0
},
"scale": {
"x": 1,
"y": 1,
"z": 1
}
},
"Camera": {
"position": {
"y": -3.4561481429695853,
"x": -0.1803637517299077,
"z": 0
},
"rotation": {
"x": 0,
"y": 0,
"z": 0
},
"scale": {
"x": 1,
"y": 1,
"z": 1
}
},
"Spotlight": {
"position": {
"x": 0,
"y": 0.34178271862927456,
"z": 0
},
"rotation": {
"x": 0,
"y": 0,
"z": 0
},
"scale": {
"x": 1,
"y": 1,
"z": 1
},
"intensity": 10
}
}
},
"sequence": {
"subUnitsPerUnit": 30,
"length": 4,
"type": "PositionalSequence",
"tracksByObject": {
"Box": {
"trackData": {
"SxW5NK1Z35": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"position\",\"x\"]",
"keyframes": [
{
"id": "mERu3kt0Xs",
"position": 0,
"connectedRight": true,
"handles": [
0.5,
1,
0.25,
0.46
],
"type": "bezier",
"value": 0
},
{
"id": "VBPb5E3B1C",
"position": 2,
"connectedRight": true,
"handles": [
0.45,
0.94,
0.55,
0.085
],
"type": "bezier",
"value": 1.9823485267067853
},
{
"id": "rfLGURCJkj",
"position": 4,
"connectedRight": true,
"handles": [
0.68,
0.53,
0.5,
0
],
"type": "bezier",
"value": 0.32792832756835955
}
]
},
"kOMLbXUiZO": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"position\",\"y\"]",
"keyframes": [
{
"id": "-A5U2JRANH",
"position": 0,
"connectedRight": true,
"handles": [
0.5,
1,
0.25,
0.46
],
"type": "bezier",
"value": 0
},
{
"id": "T-TNfaz7r4",
"position": 2,
"connectedRight": true,
"handles": [
0.45,
0.94,
0.55,
0.085
],
"type": "bezier",
"value": 2.240748284796251
},
{
"id": "KloOKSYvgL",
"position": 4,
"connectedRight": true,
"handles": [
0.68,
0.53,
0.5,
0
],
"type": "bezier",
"value": 3.6974259955062565
}
]
},
"pTB8Ik7IJ5": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"position\",\"z\"]",
"keyframes": [
{
"id": "5ik8ooLc9L",
"position": 0,
"connectedRight": true,
"handles": [
0.5,
1,
0.25,
0.46
],
"type": "bezier",
"value": 0
},
{
"id": "G1lA8oVdD6",
"position": 2,
"connectedRight": true,
"handles": [
0.45,
0.94,
0.55,
0.085
],
"type": "bezier",
"value": 2.245397305639063
},
{
"id": "cfuJSupHjD",
"position": 4,
"connectedRight": true,
"handles": [
0.68,
0.53,
0.5,
0
],
"type": "bezier",
"value": -0.37185537115063294
}
]
},
"Up7d7ZCZpA": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"rotation\",\"x\"]",
"keyframes": [
{
"id": "kVu_NY2i2n",
"position": 0,
"connectedRight": true,
"handles": [
0.5,
1,
0.25,
0.46
],
"type": "bezier",
"value": 0
},
{
"id": "L1BcPcoiNp",
"position": 2,
"connectedRight": true,
"handles": [
0.45,
0.94,
0.55,
0.085
],
"type": "bezier",
"value": -17
},
{
"id": "WeEOQ43olV",
"position": 4,
"connectedRight": true,
"handles": [
0.68,
0.53,
0.5,
0
],
"type": "bezier",
"value": 103
}
]
},
"RBXXUWXZPY": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"rotation\",\"y\"]",
"keyframes": [
{
"id": "4AEPToo_xo",
"position": 0,
"connectedRight": true,
"handles": [
0.5,
1,
0.25,
0.46
],
"type": "bezier",
"value": 0
},
{
"id": "WeFeWY1Ck0",
"position": 2,
"connectedRight": true,
"handles": [
0.45,
0.94,
0.55,
0.085
],
"type": "bezier",
"value": 83
},
{
"id": "aaBFIqYEFC",
"position": 4,
"connectedRight": true,
"handles": [
0.68,
0.53,
0.5,
0
],
"type": "bezier",
"value": 135
}
]
},
"4rTUFL8Rjv": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"rotation\",\"z\"]",
"keyframes": [
{
"id": "HYpJMJt8Dw",
"position": 0,
"connectedRight": true,
"handles": [
0.5,
1,
0.25,
0.46
],
"type": "bezier",
"value": 0
},
{
"id": "RLo4B-EMu3",
"position": 2,
"connectedRight": true,
"handles": [
0.45,
0.94,
0.55,
0.085
],
"type": "bezier",
"value": 43
},
{
"id": "P1hzyC7J7H",
"position": 4,
"connectedRight": true,
"handles": [
0.68,
0.53,
0.5,
0
],
"type": "bezier",
"value": 19
}
]
}
},
"trackIdByPropPath": {
"[\"position\",\"x\"]": "SxW5NK1Z35",
"[\"position\",\"y\"]": "kOMLbXUiZO",
"[\"position\",\"z\"]": "pTB8Ik7IJ5",
"[\"rotation\",\"x\"]": "Up7d7ZCZpA",
"[\"rotation\",\"y\"]": "RBXXUWXZPY",
"[\"rotation\",\"z\"]": "4rTUFL8Rjv"
}
}
}
}
}
},
"definitionVersion": "0.4.0",
"revisionHistory": [
"U-YSeMCq-p7is9Th",
"sCzKmBDPVWZIRTsi",
"BKd5QQO-QfV2Qu4-"
]
}

View File

@ -0,0 +1,8 @@
<script lang="ts">
import { Root, Container } from 'threlte-uikit';
</script>
<Root backgroundColor="red" sizeX={8} sizeY={4} flexDirection="row">
<Container flexGrow={1} margin={32} backgroundColor="green" />
<Container flexGrow={1} margin={32} backgroundColor="blue" />
</Root>

View File

@ -1,26 +1,22 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import type { SearchResult } from './utils/search'; import type { SearchResult } from './utils/search';
import type { HumanMessage, AIMessage, SystemMessage } from '@langchain/core/messages'; import { goto } from '$app/navigation';
const initArray: SearchResult[] = []; function createSearchStore() {
export const searchResults = writable(initArray); const { subscribe, set } = writable<SearchResult[]>([]);
export type ChatHistory = (HumanMessage | AIMessage | SystemMessage)[]; return {
subscribe,
const chatHistories: Record<string, ChatHistory> = {}; set: (results: SearchResult[]) => {
set(results);
export const chatStore = writable(chatHistories); if (results.length > 0) {
goto('/search');
export function getChatHistory(sessionId: string): ChatHistory { }
return chatHistories[sessionId] || []; },
clear: () => {
set([]);
}
};
} }
export function setChatHistory(sessionId: string, history: ChatHistory): void { export const searchResults = createSearchStore();
chatHistories[sessionId] = history;
chatStore.set(chatHistories);
}
export function clearChatHistory(sessionId: string): void {
chatHistories[sessionId] = [];
chatStore.set(chatHistories);
}

View File

@ -1,4 +1,4 @@
<script context="module"> <script module>
import { tick } from 'svelte'; import { tick } from 'svelte';
/** /**
@ -51,13 +51,17 @@
</script> </script>
<script> <script>
/** /**
* DOM Element or CSS Selector * @typedef {Object} Props
* @type { HTMLElement|string} * @property { HTMLElement|string} [target] - DOM Element or CSS Selector
* @property {import('svelte').Snippet} [children]
*/ */
export let target = 'body';
/** @type {Props} */
let { target = 'body', children } = $props();
</script> </script>
<div use:portal={target} hidden style="display: contents;"> <div use:portal={target} hidden style="display: contents;">
<slot /> {@render children?.()}
</div> </div>

View File

@ -1,18 +1,13 @@
<script context="module"> <script module>
import Headline from './poetry/h1.svelte'; import Headline from './poetry/h1.svelte';
import p from './poetry/p.svelte'; import p from './poetry/p.svelte';
export { Headline as h1, p }; export { Headline as h1, p };
</script> </script>
<script lang="ts"> <script lang="ts">
export let title; let { title, date, categories, tags, year, layout, children } = $props();
export let date;
export let categories;
export let tags;
export let year;
export let layout;
</script> </script>
<main> <main>
<slot /> {@render children?.()}
</main> </main>

View File

@ -1,18 +1,21 @@
<script context="module"> <script module>
import Headline from './thoughts/h1.svelte'; import Headline from './thoughts/h1.svelte';
import p from './thoughts/p.svelte'; import p from './thoughts/p.svelte';
export { Headline as h1, p }; export { Headline as h1, p };
</script> </script>
<script lang="ts"> <script lang="ts">
export let title; let {
export let date; title,
export let categories; date,
export let tags; categories,
export let year; tags,
export let layout; year,
layout,
children
} = $props();
</script> </script>
<main> <main>
<slot /> {@render children?.()}
</main> </main>

View File

@ -1,3 +1,5 @@
import { render } from 'svelte/server';
export interface Metadata { export interface Metadata {
title: string; title: string;
date: string; date: string;
@ -23,9 +25,8 @@ export interface Post {
interface Data { interface Data {
metadata: Metadata; metadata: Metadata;
default: { // eslint-disable-next-line @typescript-eslint/no-explicit-any
render: () => { html: string }; default: any;
};
} }
function isData(obj: unknown): obj is Data { function isData(obj: unknown): obj is Data {
@ -74,9 +75,8 @@ export const fetchMarkdownPosts = async (
return undefined; return undefined;
} }
const { metadata } = data; const { metadata } = data;
const { html } = data.default.render(); const { body } = render(data.default, {});
// remove html tags const content = body.replace(/<[^>]*>/g, '');
const content = html.replace(/<[^>]*>/g, '');
const section = path.split('/')[3]; const section = path.split('/')[3];
const filename = path.split('/').pop()?.slice(0, -3); const filename = path.split('/').pop()?.slice(0, -3);

View File

@ -1,3 +1,11 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<h1 class="poetry-headline"> <h1 class="poetry-headline">
<slot></slot> {@render children?.()}
</h1> </h1>

View File

@ -1,3 +1,11 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<p class="whitespace-pre-wrap"> <p class="whitespace-pre-wrap">
<slot></slot> {@render children?.()}
</p> </p>

View File

@ -1,3 +1,11 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<h1 class="poetry-headline"> <h1 class="poetry-headline">
<slot></slot> {@render children?.()}
</h1> </h1>

View File

@ -1,3 +1,11 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<p class="whitespace-pre-wrap"> <p class="whitespace-pre-wrap">
<slot></slot> {@render children?.()}
</p> </p>

View File

@ -1,10 +1,13 @@
<script lang="ts"> <script lang="ts">
import '../../app.css'; import '../../app.css';
import SearchResults from '$lib/components/SearchResults.svelte';
import AppContainer from '$lib/components/scenes/app/AppContainer.svelte'; import AppContainer from '$lib/components/scenes/app/AppContainer.svelte';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script> </script>
<AppContainer> <AppContainer>
<slot /> {@render children?.()}
<SearchResults />
</AppContainer> </AppContainer>

View File

@ -1,14 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { searchResults } from '$lib/store';
import type { SearchResult } from '$lib/utils/search';
let results: SearchResult[] = [];
searchResults.subscribe((value: SearchResult[]) => {
results = value ? value : [];
});
type Greeting = { type Greeting = {
greeting: string; greeting: string;
@ -40,12 +32,11 @@
let greetings = [...GREETINGS]; let greetings = [...GREETINGS];
let currentGreeting: Greeting = { greeting: 'Hello', language: 'English' }; let currentGreeting: Greeting = $state({ greeting: 'Hello', language: 'English' });
let visible = false; let visible = $state(false);
onMount(() => { onMount(() => {
visible = true; visible = true;
searchResults.set([]);
const interval = setInterval(getRandomGreeting, 3000); const interval = setInterval(getRandomGreeting, 3000);
return () => clearInterval(interval); return () => clearInterval(interval);
}); });
@ -72,49 +63,47 @@
<title>silentsilas - Home</title> <title>silentsilas - Home</title>
</svelte:head> </svelte:head>
{#if results.length <= 0} <div class="mx-auto container flex flex-col flex-1 my-4">
<div class="mx-auto container flex flex-col flex-1 my-4"> <div class="my-4 text-center" style="height: 60px">
<div class="my-4 text-center" style="height: 60px"> {#if visible && currentGreeting}
{#if visible && currentGreeting} <div transition:fade={{ duration: 1200 }}>
<div transition:fade={{ duration: 1200 }}> <span class="font-bold text-primary">{currentGreeting.greeting}</span>
<span class="font-bold text-primary">{currentGreeting.greeting}</span> {#if currentGreeting.romanisation}
{#if currentGreeting.romanisation} <span>( {currentGreeting.romanisation} )</span>
<span>( {currentGreeting.romanisation} )</span> {/if}
{/if} </div>
</div> <p transition:fade={{ delay: 400, duration: 400 }}>
<p transition:fade={{ delay: 400, duration: 400 }}> That's {currentGreeting.language} for hello!
That's {currentGreeting.language} for hello!
</p>
{/if}
</div>
<div class="prose p-4" style="align-self: center">
<p>
The name's Silas. I write code for a living, and sometimes for fun. I use <a
href="https://elixir-lang.org/"
target="_blank"
class="link-primary">Elixir</a
>
at my day job, and recently have been messing around with
<a href="https://www.rust-lang.org/" target="_blank" class="link-primary">Rust</a>,
<a href="https://kit.svelte.dev/" target="_blank" class="link-primary">Svelte</a>, and
<a href="https://threejs.org/" target="_blank" class="link-primary">three.js</a>
</p> </p>
<p> {/if}
Here you can browse my shower <a href="/thoughts" class="link-primary">thoughts</a> and bad
<a href="/poetry" class="link-primary">poetry</a>. Opinions are personally mine and not
endorsed by my employer.
</p>
<p>
I tend to start a lot of <a href="/projects" class="link-primary">projects</a>, but I'm
trying to finish more. This will also host any weird web experiments that I think others
might find interesting.
</p>
<p>
I self-host a lot of <a href="/services" class="link-primary">services</a> I find useful. None
of them run any analytics or log your activity, but the software/servers may be outdated, so
use at your own risk.
</p>
<p class="text-center">Shalom.</p>
</div>
</div> </div>
{/if} <div class="prose p-4" style="align-self: center">
<p>
The name's Silas. I write code for a living, and sometimes for fun. I use <a
href="https://elixir-lang.org/"
target="_blank"
class="link-primary">Elixir</a
>
at my day job, and recently have been messing around with
<a href="https://www.rust-lang.org/" target="_blank" class="link-primary">Rust</a>,
<a href="https://kit.svelte.dev/" target="_blank" class="link-primary">Svelte</a>, and
<a href="https://threejs.org/" target="_blank" class="link-primary">three.js</a>
</p>
<p>
Here you can browse my shower <a href="/thoughts" class="link-primary">thoughts</a> and bad
<a href="/poetry" class="link-primary">poetry</a>. Opinions are personally mine and not
endorsed by my employer.
</p>
<p>
I tend to start a lot of <a href="/projects" class="link-primary">projects</a>, but I'm trying
to finish more. This will also host any weird web experiments that I think others might find
interesting.
</p>
<p>
I self-host a lot of <a href="/services" class="link-primary">services</a> I find useful. None
of them run any analytics or log your activity, but the software/servers may be outdated, so use
at your own risk.
</p>
<p class="text-center">Shalom.</p>
</div>
</div>

View File

@ -1,87 +1,12 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import PaginatedPosts from '$lib/components/PaginatedPosts.svelte';
import { page } from '$app/stores'; import type { PageData } from './$types';
import { searchResults } from '$lib/store';
import type { SearchResult } from '$lib/utils/search';
import { onMount } from 'svelte';
import type { PageData } from '../poetry/$types';
export let data: PageData;
let results: SearchResult[] = []; let { data }: { data: PageData } = $props();
searchResults.subscribe((value: SearchResult[]) => {
results = value ? value : [];
});
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
let { posts, total } = data;
const limit = 8;
let currentPage = Number($page.url.searchParams.get('page')) || 1;
let totalPages = Math.ceil(total / limit);
$: $page.url.searchParams.get('page'),
(currentPage = Number($page.url.searchParams.get('page')) || 1);
async function fetchData(page: number) {
const response = await fetch(`/api/poetry?limit=${limit}&page=${page}`);
const newData = await response.json();
currentPage = page;
posts = newData.posts;
total = newData.total;
totalPages = Math.ceil(total / limit);
}
onMount(() => {
searchResults.set([]);
});
function navigate(page: number) {
fetchData(page);
goto(`/poetry/?page=${page}`, { replaceState: true });
}
</script> </script>
<svelte:head> <svelte:head>
<title>silentsilas - Poetry</title> <title>silentsilas - Poetry</title>
</svelte:head> </svelte:head>
{#if results.length <= 0} <PaginatedPosts {data} baseUrl="poetry" title="Poetry" />
<div class="container mx-auto flex flex-col items-center">
<div class="prose mb-4">
<h1 class="py-6">Poetry</h1>
</div>
<ul>
{#each posts as post}
<li class="py-4">
<h3 class="pb-1">
<a class="link-primary" href={`/poetry/${post.filename}`}>
{post.meta.title}
</a>
</h3>
<p class="text-sm">{formatDate(post.meta.date)}</p>
</li>
{/each}
</ul>
<nav class="join justify-end py-10">
<button
class="join-item btn-primary btn btn-outline"
on:click={() => navigate(currentPage - 1)}
disabled={currentPage === 1}>Prev</button
>
<div class="join-item content-center px-10">{currentPage} of {totalPages}</div>
<button
class="join-item btn btn-primary btn-outline"
on:click={() => navigate(currentPage + 1)}
disabled={currentPage === totalPages}>Next</button
>
</nav>
</div>
{/if}

View File

@ -1,30 +1,20 @@
<script lang="ts"> <script lang="ts">
import { searchResults } from '$lib/store';
import type { SearchResult } from '$lib/utils/search';
import { onMount } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; interface Props {
data: PageData;
}
let { data }: Props = $props();
const { title, date: _date, Content, categories: _ } = data; const { title, date: _date, Content, categories: _ } = data;
let results: SearchResult[] = [];
searchResults.subscribe((value: SearchResult[]) => {
results = value ? value : [];
});
onMount(() => {
searchResults.set([]);
});
</script> </script>
<svelte:head> <svelte:head>
<title>silentsilas - {title}</title> <title>silentsilas - {title}</title>
</svelte:head> </svelte:head>
{#if results.length <= 0} <div class="container mx-auto flex flex-col items-center prose px-4">
<div class="container mx-auto flex flex-col items-center prose px-4"> <h1 class="py-6 mb-0">{title}</h1>
<h1 class="py-6 mb-0">{title}</h1> <Content />
<Content /> <a href="/poetry" class="link-primary py-10">Back to Poetry</a>
<a href="/poetry" class="link-primary py-10">Back to Poetry</a> </div>
</div>
{/if}

View File

@ -1,13 +1,4 @@
<script lang="ts"> <script lang="ts">
import type { SearchResult } from '$lib/utils/search';
import { searchResults } from '$lib/store';
let results: SearchResult[] = [];
searchResults.subscribe((value: SearchResult[]) => {
results = value ? value : [];
});
const selfhostedServices = [ const selfhostedServices = [
{ {
title: 'Nextcloud', title: 'Nextcloud',
@ -52,28 +43,26 @@
<title>silentsilas - Services</title> <title>silentsilas - Services</title>
</svelte:head> </svelte:head>
{#if results.length <= 0} <div class="container mx-auto flex flex-col items-center px-4">
<div class="container mx-auto flex flex-col items-center px-4"> <div class="prose">
<div class="prose"> <h1 class="pt-6 text-center">Services</h1>
<h1 class="pt-6 text-center">Services</h1> <p>
<p> I self-host a lot of services I find useful, and to rely less on third-party cloud services.
I self-host a lot of services I find useful, and to rely less on third-party cloud services. None of them run any analytics or log your activity, but the software/servers may be outdated,
None of them run any analytics or log your activity, but the software/servers may be so use at your own risk.
outdated, so use at your own risk. </p>
</p>
<ul> <ul>
{#each selfhostedServices as service} {#each selfhostedServices as service}
<li> <li>
<h3> <h3>
<a class="link-primary" href={service.url} target="_blank"> <a class="link-primary" href={service.url} target="_blank">
{service.title} {service.title}
</a> </a>
</h3> </h3>
<p class="text-sm">{service.description}</p> <p class="text-sm">{service.description}</p>
</li> </li>
{/each} {/each}
</ul> </ul>
</div>
</div> </div>
{/if} </div>

View File

@ -1,89 +1,12 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import PaginatedPosts from '$lib/components/PaginatedPosts.svelte';
import { page } from '$app/stores'; import type { PageData } from './$types';
import { searchResults } from '$lib/store';
import type { SearchResult } from '$lib/utils/search';
import { onMount } from 'svelte';
import type { PageData } from '../thoughts/$types';
export let data: PageData;
let results: SearchResult[] = []; let { data }: { data: PageData } = $props();
searchResults.subscribe((value: SearchResult[]) => {
results = value ? value : [];
});
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
let { posts, total } = data;
const limit = 8;
let currentPage = Number($page.url.searchParams.get('page')) || 1;
let totalPages = Math.ceil(total / limit);
$: $page.url.searchParams.get('page'),
(currentPage = Number($page.url.searchParams.get('page')) || 1);
async function fetchData(page: number) {
const response = await fetch(`/api/thoughts?limit=${limit}&page=${page}`);
const newData = await response.json();
currentPage = page;
posts = newData.posts;
total = newData.total;
totalPages = Math.ceil(total / limit);
}
onMount(() => {
searchResults.set([]);
});
function navigate(page: number) {
fetchData(page);
goto(`/thoughts/?page=${page}`, { replaceState: true });
}
</script> </script>
<svelte:head> <svelte:head>
<title>silentsilas - Thoughts</title> <title>silentsilas - Thoughts</title>
</svelte:head> </svelte:head>
{#if results.length <= 0} <PaginatedPosts {data} baseUrl="thoughts" title="Thoughts" />
<div class="container mx-auto flex flex-col items-center">
<div class="prose">
<h1 class="py-6 pt-10">Thoughts</h1>
</div>
<ul>
{#each posts as post}
<li class="py-4">
<h3 class="pb-1">
<a class="link-primary" href={`/thoughts/${post.filename}`}>
{post.meta.title}
</a>
</h3>
<p class="text-sm">{formatDate(post.meta.date)}</p>
</li>
{/each}
</ul>
{#if total > 1}
<nav class="join justify-end py-4">
<button
class="join-item btn-primary btn btn-outline"
on:click={() => navigate(currentPage - 1)}
disabled={currentPage === 1}>Prev</button
>
<div class="join-item content-center px-10">{currentPage} of {totalPages}</div>
<button
class="join-item btn btn-primary btn-outline"
on:click={() => navigate(currentPage + 1)}
disabled={currentPage === totalPages}>Next</button
>
</nav>
{/if}
</div>
{/if}

View File

@ -1,30 +1,20 @@
<script lang="ts"> <script lang="ts">
import { searchResults } from '$lib/store';
import type { SearchResult } from '$lib/utils/search';
import { onMount } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; interface Props {
data: PageData;
}
let { data }: Props = $props();
const { title, date: _date, Content, categories: _ } = data; const { title, date: _date, Content, categories: _ } = data;
let results: SearchResult[] = [];
searchResults.subscribe((value: SearchResult[]) => {
results = value ? value : [];
});
onMount(() => {
searchResults.set([]);
});
</script> </script>
<svelte:head> <svelte:head>
<title>silentsilas - {title}</title> <title>silentsilas - {title}</title>
</svelte:head> </svelte:head>
{#if results.length <= 0} <div class="container mx-auto flex flex-col items-center prose px-4">
<div class="container mx-auto flex flex-col items-center prose px-4"> <h1 class="pt-10">{title}</h1>
<h1 class="pt-10">{title}</h1> <Content />
<Content /> <a href="/thoughts" class="link-primary py-10">Back to Thoughts</a>
<a href="/thoughts" class="link-primary py-10">Back to Thoughts</a> </div>
</div>
{/if}

View File

@ -1 +1,9 @@
<slot /> <script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
{@render children?.()}

View File

@ -1,9 +1,12 @@
<script lang="ts"> <script lang="ts">
import SearchResults from '$lib/components/SearchResults.svelte';
import AppContainer from '$lib/components/scenes/app/AppContainer.svelte'; import AppContainer from '$lib/components/scenes/app/AppContainer.svelte';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script> </script>
<AppContainer> <AppContainer>
<slot /> {@render children?.()}
<SearchResults />
</AppContainer> </AppContainer>

View File

@ -1,164 +1,88 @@
<script lang="ts"> <script lang="ts">
import '../../app.css'; import '../../app.css';
import type { SearchResult } from '$lib/utils/search';
import { searchResults, type ChatHistory } from '$lib/store';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { marked } from 'marked'; import { marked } from 'marked';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import { PUBLIC_LOAD_DUMMY_HISTORY } from '$env/static/public';
import Toast from '$lib/components/Toast.svelte'; import Toast from '$lib/components/Toast.svelte';
import { HumanMessage, type AIMessage } from '@langchain/core/messages';
import { ChatManager } from '$lib/components/ai/ChatManager';
import Keys from '$lib/components/ai/Settings.svelte';
let searchResultsValue: SearchResult[] = []; let query = $state('');
let query = ''; let loading = $state(false);
let loading = false; let showToast = $state(false);
let showToast = false; let toastMessage = $state('');
let toastMessage = ''; let showSettings = $state(false);
type Questions = { let chatManager: ChatManager;
[key: string]: string[]; let chatHistory: (HumanMessage | AIMessage)[] = $state([]);
};
const QUESTIONS: Questions = { onMount(() => {
brewing: [ initializeChatManager();
'What is the difference between Western and Eastern styles of tea brewing?', });
'How does water temperature and steeping time affect the flavor of different types of tea?',
'What are some traditional tea brewing methods from around the world?',
'What is gongfu cha (功夫茶), and how does it differ from other tea preparation methods?'
],
varieties: [
'What is the difference between raw and ripe pu-erh tea, and how does aging affect their taste and value?',
'Can you explain the grading system for teas, particularly for varieties like Darjeeling and Assam?',
'What are some rare or unusual tea varieties and what makes them unique?',
'How do flavored teas (like Earl Grey or Jasmine) differ in production and taste from pure teas?'
],
history: [
'How did tea influence global trade routes and international relations, particularly between China, Britain, and India?',
'What role did Buddhism play in the spread of tea culture throughout Asia?',
"How did the British East India Company's involvement in the tea trade impact both India and China?",
'How did the Opium Wars, which were closely tied to the tea trade, alter the course of Chinese history?',
'What role did tea play in the American Revolution, and how did this event change tea consumption patterns in America?'
],
culture: [
'In what ways did tea ceremonies in different cultures (like Japan and England) reflect and shape social norms?',
'How did the democratization of tea drinking in Europe affect social structures and daily life?',
'How has the perception of tea as a medicinal substance evolved from ancient times to the present day?',
'What are some of the most popular tea-related idioms and proverbs, and what do they mean?'
],
production: [
'How did the processing and preparation of tea evolve over time, and what factors influenced these changes?',
'What impact did tea plantations have on local ecosystems and labor practices in countries like India and Sri Lanka?',
'What are the main differences in production methods between black, green, oolong, and white teas?',
'How does the terroir (environmental factors) affect the flavor and quality of tea?'
]
};
function generateDummyHistory() { function initializeChatManager() {
return [
JSON.parse(JSON.stringify(new HumanMessage({ content: 'Hello, AI!' }))),
JSON.parse(JSON.stringify(new AIMessage({ content: 'Hello! How can I assist you today?' }))),
JSON.parse(JSON.stringify(new HumanMessage({ content: "What's the weather like?" }))),
JSON.parse(
JSON.stringify(
new AIMessage({
content:
"I'm sorry, but I don't have access to real-time weather information. You might want to check a weather app or website for the most up-to-date forecast."
})
)
)
];
}
let chatHistory: ChatHistory = [];
onMount(async () => {
try { try {
if (PUBLIC_LOAD_DUMMY_HISTORY === 'true') { chatManager = new ChatManager();
chatHistory = generateDummyHistory(); chatHistory = chatManager.getChatHistory();
return;
}
const response = await fetch('/api/ai');
const data = await response.json();
chatHistory = data.chatHistory || [];
} catch (error) { } catch (error) {
console.error('Error fetching chat history:', error); console.error('Error initializing chat manager:', error);
showError('Please set up your API key');
showSettings = true;
} }
});
searchResults.subscribe((value: SearchResult[]) => {
searchResultsValue = value ? value : [];
});
function getRoleAndContent(message: any): { role: string; content: string } {
if (message.type === 'constructor') {
const messageType = message.id[2];
return {
role: messageType.replace('Message', '').toLowerCase(),
content: message.kwargs.content
};
}
return {
role: 'unknown',
content: JSON.stringify(message)
};
} }
// safely render markdown function showError(message: string) {
toastMessage = message;
showToast = true;
}
function getMessageContent(message: HumanMessage | AIMessage): string {
const content = message.content;
if (typeof content === 'string') {
return content;
}
if (Array.isArray(content)) {
return content
.map((item) => {
if (typeof item === 'string') {
return item as string;
}
return item || ('' as string);
})
.join(' ');
}
return '';
}
function renderMarkdown(content: string) { function renderMarkdown(content: string) {
return marked(content); return marked(content);
} }
async function handleSubmit() { async function handleSubmit(event: SubmitEvent) {
event.preventDefault();
if (!query.trim()) return;
loading = true; loading = true;
try { try {
const response = await fetch('/api/ai', { const { chatHistory: newHistory } = await chatManager.sendMessage(query);
method: 'POST', chatHistory = newHistory;
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query })
});
const data = await response.json();
chatHistory = data.chatHistory || [];
query = ''; query = '';
} catch (error) { } catch (error) {
console.error('Error fetching AI response:', error); console.error('Error fetching AI response:', error);
showError('Failed to get response from AI');
} finally { } finally {
loading = false; loading = false;
} }
} }
async function handleNewSession() { async function handleNewSession() {
try { chatManager.clearHistory();
const response = await fetch('/api/ai/new-session', { method: 'POST' }); chatHistory = [];
if (response.ok) { query = '';
const historyResponse = await fetch('/api/ai');
const data = await historyResponse.json();
chatHistory = data.chatHistory || [];
query = '';
} else {
console.error('Failed to start new session');
}
} catch (error) {
console.error('Error starting new session:', error);
}
} }
function copyToClipboard(text: string) { function handleApiKeySave() {
navigator.clipboard showSettings = false;
.writeText(text) initializeChatManager();
.then(() => {
toastMessage = 'Copied to clipboard!';
showToast = true;
})
.catch((err) => {
toastMessage = 'Failed to copy text.';
showToast = true;
});
}
function handleToastClose() {
showToast = false;
} }
</script> </script>
@ -166,110 +90,70 @@
<title>silentsilas - AI</title> <title>silentsilas - AI</title>
</svelte:head> </svelte:head>
{#if searchResultsValue.length === 0} <div class="flex-grow flex-col overflow-auto p-4 my-2 bg-base-200">
<div class="flex-grow flex-col overflow-auto p-4 my-2 bg-base-200"> <div class="flex flex-col items-center gap-4">
<div class="flex flex-col items-center gap-4"> <div class="avatar">
<div class="avatar"> <div class="ring-primary ring-offset-base-100 w-64 rounded-full ring ring-offset-2">
<div class="ring-primary ring-offset-base-100 w-64 rounded-full ring ring-offset-2"> <img src="/imgs/ai/profile.jpg" alt="Portrait of an orange tabby cat reading a book" />
<img src="/imgs/ai/profile.jpg" alt="Portrait of an orange tabby cat reading a book" />
</div>
</div> </div>
<span>
This lil guy just finished reading <a
class="link link-primary"
href="https://bookshop.org/p/books/a-history-of-tea-the-life-and-times-of-the-world-s-favorite-beverage-laura-c-martin/11044690"
target="_blank"
>
A History of Tea
</a>, so ask 'em about tea or something.
</span>
</div> </div>
<div class="space-y-4"> <span> This lil guy just got out of AI school, so ask 'em about something. </span>
{#each chatHistory as message}
{@const { role, content } = getRoleAndContent(message)}
{#if role === 'human'}
<div class="chat chat-end">
<div class="chat-bubble chat-bubble-primary prose">
{@html renderMarkdown(content)}
</div>
</div>
{:else}
<div class="p-4 rounded-lg bg-base-300 prose md:container">
{@html renderMarkdown(content)}
</div>
{/if}
{/each}
</div>
{#if loading}
<div class="mt-4 flex flex-col items-center justify-center">
<span class="loading loading-dots loading-lg"></span>
<span>This may take a minute; streaming is still a work in progress.</span>
</div>
{/if}
<form on:submit|preventDefault={handleSubmit} class="mt-4 flex-col">
<label class="form-control">
<textarea
bind:value={query}
class="textarea textarea-bordered h-24"
placeholder="Type your message here..."
></textarea>
</label>
<button type="submit" class="btn btn-block btn-primary mt-4" disabled={loading}>
Send
</button>
<button
type="button"
class="btn btn-block btn-error btn-outline mt-2"
on:click={handleNewSession}
>
New Session
</button>
<div class="bg-base-200 collapse collapse-plus border-primary border mt-20">
<input type="checkbox" class="peer" />
<div class="collapse-title bg-base-200 peer-checked:bg-base-300">
Interesting things to ask:
</div>
<div class="collapse-content bg-base-200 peer-checked:bg-base-300">
{#each Object.entries(QUESTIONS) as [category, questions]}
<div class="mb-4">
<h3 class="text-lg font-semibold mb-2 capitalize">{category}</h3>
<ul class="list-disc list-inside space-y-4">
{#each questions as item}
<li class="flex items-center gap-2">
<span>{item}</span>
<button
type="button"
class="btn btn-sm btn-outline"
on:click={() => copyToClipboard(item)}
title="Copy to clipboard"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
</button>
</li>
{/each}
</ul>
</div>
{/each}
</div>
</div>
</form>
</div> </div>
{/if} <div class="space-y-4">
{#each chatHistory as message}
{#if message instanceof HumanMessage}
<div class="chat chat-end">
<div class="chat-bubble chat-bubble-primary prose">
{@html renderMarkdown(getMessageContent(message))}
</div>
</div>
{:else}
<div class="p-4 rounded-lg bg-base-300 prose md:container">
{@html renderMarkdown(getMessageContent(message))}
</div>
{/if}
{/each}
</div>
{#if loading}
<div class="mt-4 flex flex-col items-center justify-center">
<span class="loading loading-dots loading-lg"></span>
<span>This may take a minute; streaming is still a work in progress.</span>
</div>
{/if}
<form onsubmit={handleSubmit} class="mt-4 flex-col">
<label class="form-control">
<textarea
bind:value={query}
class="textarea textarea-bordered h-24"
placeholder="Type your message here..."
></textarea>
</label>
<button type="submit" class="btn btn-block btn-primary mt-4" disabled={loading}> Send </button>
<button
type="button"
class="btn btn-block btn-error btn-outline mt-2"
onclick={handleNewSession}
>
New Session
</button>
</form>
<div class="mt-2 flex-col">
<button
type="button"
class="btn btn-block btn-outline"
onclick={() => (showSettings = !showSettings)}
>
Settings
</button>
</div>
</div>
{#if showToast} {#if showToast}
<Toast message={toastMessage} type="success" on:close={handleToastClose} /> <Toast message={toastMessage} type="success" onClose={() => (showToast = false)} />
{/if}
{#if showSettings}
<Keys onClose={() => (showSettings = false)} onSave={handleApiKeySave} />
{/if} {/if}

View File

@ -1,80 +0,0 @@
import { json } from '@sveltejs/kit';
import { ChatAnthropic } from '@langchain/anthropic';
import { RunnableSequence, RunnablePassthrough } from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import type { RequestEvent } from '@sveltejs/kit';
import { EPubLoader } from "@langchain/community/document_loaders/fs/epub";
import { join } from 'path';
import { getChatHistory, setChatHistory } from '$lib/store';
import { dev } from '$app/environment';
export async function POST({ request, locals }: RequestEvent): Promise<Response> {
const { query } = await request.json();
const sessionId = locals.sessionId;
const chatHistory = getChatHistory(sessionId);
let ebookPath;
if (dev) {
ebookPath = join(process.cwd(), 'static', 'book.epub');
} else {
ebookPath = join(process.cwd(), 'client', 'book.epub');
}
const loader = new EPubLoader(ebookPath);
const docs = await loader.load();
const context = docs.map(doc => doc.pageContent).join('\n\n');
const model = new ChatAnthropic({
modelName: 'claude-3-5-sonnet-20240620',
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
});
const SYSTEM_TEMPLATE = `The following pieces of context are the book you've just read.
You are a tea guru, and the book was "A History of Tea: The Life and Times of the World's Favorite Beverage" by Laura C. Martin.
You might be asked questions about the book, or about tea in general.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
Always format your response in markdown.
----------------
Context:
{context}
Question:
{question}`;
const prompt = ChatPromptTemplate.fromMessages([
['system', SYSTEM_TEMPLATE],
...chatHistory,
['user', '{question}']
]);
const chain = RunnableSequence.from([
{
context: () => context,
question: new RunnablePassthrough(),
},
prompt,
model,
new StringOutputParser()
]);
const answer = await chain.invoke(query);
chatHistory.push(new HumanMessage({ content: query }));
chatHistory.push(new AIMessage({ content: answer }));
setChatHistory(sessionId, chatHistory);
return json({ response: answer, chatHistory });
}
export async function GET({ locals }): Promise<Response> {
const sessionId = locals.sessionId;
const chatHistory = getChatHistory(sessionId);
return json({ chatHistory });
}

View File

@ -1,8 +0,0 @@
import type { RequestEvent } from '@sveltejs/kit';
import { clearChatHistory } from '$lib/store';
export async function POST({ locals }: RequestEvent) {
const sessionId = locals.sessionId;
clearChatHistory(sessionId);
return new Response(null, { status: 204 });
}

View File

@ -5,11 +5,11 @@ export const GET = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1; const page = Number(url.searchParams.get('page')) || 1;
const limit = Number(url.searchParams.get('limit')) || 8; const limit = Number(url.searchParams.get('limit')) || 8;
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
const {posts: allPosts, total: total} = await fetchMarkdownPosts('poetry', limit, offset); const { posts: allPosts, total: total } = await fetchMarkdownPosts('poetry', limit, offset);
const sortedPosts = allPosts.sort((a, b) => { const sortedPosts = allPosts.sort((a, b) => {
return new Date(b.meta.date).getTime() - new Date(a.meta.date).getTime(); return new Date(b.meta.date).getTime() - new Date(a.meta.date).getTime();
}); });
return json({posts: sortedPosts, total: total, page: page}); return json({ posts: sortedPosts, total: total, page: page });
}; };

View File

@ -1,9 +1,12 @@
<script lang="ts"> <script lang="ts">
import SearchResults from '$lib/components/SearchResults.svelte';
import AppContainer from '$lib/components/scenes/app/AppContainer.svelte'; import AppContainer from '$lib/components/scenes/app/AppContainer.svelte';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script> </script>
<AppContainer> <AppContainer>
<slot /> {@render children?.()}
<SearchResults />
</AppContainer> </AppContainer>

View File

@ -3,18 +3,10 @@
import World from '$lib/components/scenes/app/World.svelte'; import World from '$lib/components/scenes/app/World.svelte';
import CanvasContainer from '$lib/components/scenes/app/CanvasContainer.svelte'; import CanvasContainer from '$lib/components/scenes/app/CanvasContainer.svelte';
import '../../app.css'; import '../../app.css';
import type { SearchResult } from '$lib/utils/search';
import { searchResults } from '$lib/store';
import Overlay from '$lib/components/scenes/app/Overlay.svelte'; import Overlay from '$lib/components/scenes/app/Overlay.svelte';
let results: SearchResult[] = []; let open = $state(false);
let open = false; let selected = $state(-1);
let selected = -1;
searchResults.subscribe((value: SearchResult[]) => {
results = value ? value : [];
});
type Project = { type Project = {
title: string; title: string;
@ -34,12 +26,12 @@
'An experiment with 3D vector math for a rudimentary simulation of gravity. You can change the strength of the gravity in the Controls menu.' 'An experiment with 3D vector math for a rudimentary simulation of gravity. You can change the strength of the gravity in the Controls menu.'
}, },
{ {
title: 'Tea Guru', title: 'Chatbot',
path: '/ai', path: '/ai',
source: 'https://git.silentsilas.com/silentsilas/playground/src/branch/main/src/routes/ai', source: 'https://git.silentsilas.com/silentsilas/playground/src/branch/main/src/routes/ai',
position: [4, -2, -4], position: [4, -2, -4],
description: description:
'A chatbot trained on an in-depth book about tea. Ask it about anything related to tea!' "Bring-your-own-key Anthropic chatbot. You can change the system prompt, and the chat history is saved locally. Otherwise it's nothing too fancy."
}, },
{ {
title: 'Headbang', title: 'Headbang',
@ -88,55 +80,53 @@
<title>silentsilas - Projects</title> <title>silentsilas - Projects</title>
</svelte:head> </svelte:head>
{#if results.length <= 0} <CanvasContainer>
<CanvasContainer> <World>
<World> {#each projects as project, i}
{#each projects as project, i} <MenuItem
<MenuItem clickHandler={() => handleMenuClick(i)}
clickHandler={() => handleMenuClick(i)} position={project.position}
position={project.position} active={i === selected}>{project.title}</MenuItem
active={i === selected}>{project.title}</MenuItem
>
{/each}
</World>
</CanvasContainer>
{#if open}
<Overlay>
<button
type="button"
class="close-button p-4 m-4 btn btn-outline"
on:click={() => {
open = false;
selected = -1;
}}>X</button
> >
<div class="p-6"> {/each}
<h2 class="mt-0">{projects[selected].title}</h2> </World>
<p>{projects[selected].description}</p> </CanvasContainer>
<div class="flex justify-evenly"> {#if open}
{#if projects[selected].source === '#'} <Overlay>
<button class="btn btn-primary sm:btn-sm md:btn-wide" disabled <button
>Source Code Unavailable</button type="button"
> class="close-button p-4 m-4 btn btn-outline"
{:else} onclick={() => {
<a open = false;
href={projects[selected].source} selected = -1;
target={isExternal(projects[selected].source) ? '_blank' : ''} }}>X</button
> >
<button class="btn btn-primary sm:btn-sm md:btn-wide">Source Code</button> <div class="p-6">
</a> <h2 class="mt-0">{projects[selected].title}</h2>
{/if} <p>{projects[selected].description}</p>
<div class="flex justify-evenly">
{#if projects[selected].source === '#'}
<button class="btn btn-primary sm:btn-sm md:btn-wide" disabled
>Source Code Unavailable</button
>
{:else}
<a <a
href={projects[selected].path} href={projects[selected].source}
target={isExternal(projects[selected].path) ? '_blank' : ''} target={isExternal(projects[selected].source) ? '_blank' : ''}
> >
<button class="btn btn-primary sm:btn-sm md:btn-wide">Visit</button></a <button class="btn btn-primary sm:btn-sm md:btn-wide">Source Code</button>
> </a>
</div> {/if}
<a
href={projects[selected].path}
target={isExternal(projects[selected].path) ? '_blank' : ''}
>
<button class="btn btn-primary sm:btn-sm md:btn-wide">Visit</button></a
>
</div> </div>
</Overlay> </div>
{/if} </Overlay>
{/if} {/if}
<style> <style>

View File

@ -0,0 +1,13 @@
<script lang="ts">
import '../../app.css';
import AppContainer from '$lib/components/scenes/app/AppContainer.svelte';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<AppContainer>
{@render children?.()}
</AppContainer>

View File

@ -3,7 +3,7 @@
import type { SearchResult } from '$lib/utils/search'; import type { SearchResult } from '$lib/utils/search';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
let results: SearchResult[] = []; let results = $searchResults;
searchResults.subscribe((value: SearchResult[]) => { searchResults.subscribe((value: SearchResult[]) => {
results = value ? value : []; results = value ? value : [];
@ -22,6 +22,10 @@
}); });
</script> </script>
<svelte:head>
<title>silentsilas - Search Results</title>
</svelte:head>
{#if results.length > 0} {#if results.length > 0}
<div class="container mx-auto flex flex-col items-center"> <div class="container mx-auto flex flex-col items-center">
<div class="prose"> <div class="prose">
@ -44,4 +48,10 @@
{/each} {/each}
</ul> </ul>
</div> </div>
{:else}
<div class="container mx-auto flex flex-col items-center">
<div class="prose">
<p>No results found</p>
</div>
</div>
{/if} {/if}

View File

@ -6,13 +6,16 @@ import { mdsvex } from 'mdsvex';
const config = { const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors // Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors // for more information about preprocessors
preprocess: [vitePreprocess(), mdsvex({ preprocess: [
extensions: ['.md'], layout: { vitePreprocess(),
poetry: './src/lib/utils/PoetryLayout.svelte', mdsvex({
thoughts: './src/lib/utils/ThoughtsLayout.svelte', extensions: ['.md'],
} layout: {
})], poetry: './src/lib/utils/PoetryLayout.svelte',
thoughts: './src/lib/utils/ThoughtsLayout.svelte'
}
})
],
kit: { kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.

View File

@ -1,12 +1,13 @@
import { sveltekit } from '@sveltejs/kit/vite' import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config' import { defineConfig } from 'vitest/config';
import { threlteStudio } from '@threlte/studio/vite';
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()], plugins: [threlteStudio(), sveltekit()],
ssr: { ssr: {
noExternal: ['three', 'three-inspect'] noExternal: ['three', 'three-inspect']
}, },
test: { test: {
include: ['src/**/*.{test,spec}.{js,ts}'] include: ['src/**/*.{test,spec}.{js,ts}']
} }
}) });